# Authentication
Source: https://developers.notion.com/cli/get-started/authentication
Log in to your Notion workspace and manage CLI credentials.
## Log in
Authenticate with your Notion workspace:
```bash theme={null}
ntn login
```
This opens your browser to an authorization page. Confirm that the code in the browser matches the code printed in your terminal before approving. This prevents another page from completing the login in your name.
Your workspace-scoped token will be stored securely in your system's keychain.
If you've already logged in to one or more workspaces, you can pick existing workspace to switch the default, or pick **Authenticate with new workspace** to start a fresh browser flow and add another workspace.
`ntn login` requires full workspace membership. [Guests](https://www.notion.com/help/whos-who-in-a-workspace) and [restricted members](https://www.notion.com/help/whos-who-in-a-workspace) cannot log in with the Notion CLI. If you need CLI access, ask a workspace admin to upgrade your role. See [Personal access tokens](/guides/get-started/personal-access-tokens) for more on who can create tokens.
## Log in without a browser
On a remote machine, container, or CI runner that can't open a browser, use `--no-browser` to get a two-step login flow:
1. Run `ntn login --no-browser`. It prints a URL, a verification code, and a `ntn login poll` command.
2. Open the URL in any browser, sign in, and confirm the verification code.
3. Run `ntn login poll` on the original machine to redeem the token.
`ntn login` also falls back to this flow automatically when it detects there is no terminal (e.g. piped input).
Login sessions expire after a short window. If polling fails because the session expired, run `ntn login` again to start over.
For unattended use (CI, scripts, bots), prefer a [personal access token](#use-a-personal-access-token) instead.
## Target a specific workspace
To run a single command against a non-default workspace without switching defaults, set `NOTION_WORKSPACE_ID`:
```bash theme={null}
NOTION_WORKSPACE_ID= ntn api v1/users/me
```
Workspace IDs are listed in the output of `ntn debug`.
## Use a personal access token
For unattended use, authenticate with a [personal access token](/guides/get-started/personal-access-tokens) (PAT) by exporting it as `NOTION_API_TOKEN`:
```bash theme={null}
export NOTION_API_TOKEN=ntn_xxx...
ntn api v1/users/me
```
`NOTION_API_TOKEN` takes precedence over anything stored in the keychain, so the same shell can mix `ntn login`-based commands and PAT-based commands depending on what's exported.
## Inspect your session
```bash theme={null}
ntn doctor
```
## Log out
```bash theme={null}
ntn logout
```
This forgets every cached workspace, deletes each one's token from the keychain, and clears the default workspace. The `config.json` and `workspaces.json` files themselves stay in place — run `ntn login` to repopulate them.
## Where credentials are stored
Tokens live in your OS credential store (Keychain on macOS, Secret Service on Linux) under the service name `notion-cli`, with the workspace ID as the account.
Two files sit alongside them in the CLI config directory:
* `config.json` — CLI version, default workspace per, and the optional `keyring` toggle.
* `workspaces.json` — cached workspace IDs and names for the interactive picker.
The config directory is `NOTION_HOME` if set, otherwise `$XDG_CONFIG_HOME/notion`, `$HOME/.config/notion`, or `$HOME/.notion` as fallbacks.
### Opt out of the OS keychain
On systems without a usable keychain, `ntn login` fails with a keychain error. Common examples include Docker containers, CI runners, SSH sessions to a Linux server, etc.
Set `NOTION_KEYRING=0` to store tokens in plain JSON at `auth.json` in the config directory instead. Treat that file like any other secret.
```bash theme={null}
NOTION_KEYRING=0 ntn login
```
To make it permanent, set `"keyring": false` in `config.json`. The env var always wins.
## Environment variables
| Variable | Purpose |
| :-------------------- | :--------------------------------------------------------------------------------------------------- |
| `NOTION_API_TOKEN` | When this is set, it'll take precedence over `ntn login`'s keychain entry. Handy for scripts and CI. |
| `NOTION_WORKSPACE_ID` | Override the default workspace for a single command. |
| `NOTION_KEYRING` | Set to `0` to use file-based storage instead of the OS keychain. |
| `NOTION_HOME` | Override the config directory. |
| `NOTION_ENV` | Same as `--env`. Rarely needed. |
Run `ntn login --help` for the full list.
## Next steps
Create and deploy your first Notion Worker.
Make Notion API requests from the terminal.
Full reference for every ntn command.
Create tokens for scripts and CI.
# Installation
Source: https://developers.notion.com/cli/get-started/installation
Install the Notion CLI on your machine.
## Install via script (recommended)
The recommended way to install `ntn` on macOS and Linux:
```bash theme={null}
curl -fsSL https://ntn.dev | bash
```
To update:
```bash theme={null}
ntn update
```
## Install via npm
Use macOS, Linux, or Windows to install:
```bash theme={null}
npm install --global ntn
```
To update:
```bash theme={null}
npm update --global ntn
```
Requires Node.js 22+ and npm 10+.
## Install via Winget (Windows)
In PowerShell or Command Prompt, including either shell in Windows Terminal:
```powershell theme={null}
winget install Notion.ntn
```
You can also confirm the WinGet package:
```powershell theme={null}
winget list --exact --id Notion.ntn
```
Restart your terminal to verify.
To update:
```powershell theme={null}
winget upgrade Notion.ntn
```
We currently only support Windows x64 (x86-64/AMD64)
## Verify installation
```bash theme={null}
ntn --version
```
## Shell completions
Enable tab completions for your shell:
```bash theme={null}
ntn completions bash # or fish, zsh, powershell, elvish
```
## Building from source
Clone the repository and use [mise](https://mise.jdx.dev/) to build a local debug binary installed as `ntnd`:
```bash theme={null}
git clone https://github.com/makenotion/cli.git
cd cli
mise build
```
See the [CLI README](https://github.com/makenotion/cli/blob/main/README.md#building-from-source) for `mise watch` and other development workflows.
## Next steps
Log in to your Notion workspace.
Create and deploy your first Notion Worker.
# Notion CLI
Source: https://developers.notion.com/cli/get-started/overview
Install the Notion CLI and learn the core commands for authentication, Notion Workers, and API requests.
`ntn` is the Notion CLI. Use it to authenticate with Notion, deploy and manage [Notion Workers](/workers/get-started/overview), and make API requests — all from your terminal.
## Install
```bash theme={null}
curl -fsSL https://ntn.dev | bash
```
Verify the installation:
```bash theme={null}
ntn --version
```
See [Installation](/cli/get-started/installation) for more installation options.
## Authenticate
Log in to connect the CLI to your Notion workspace:
```bash theme={null}
ntn login
```
This opens a browser window where you authorize access. Your credentials are stored securely in your system's keychain. See [Authentication](/cli/get-started/authentication) for more details.
## What you can do
Deploy, manage, and debug Notion Workers.
Make Notion API requests directly from the terminal.
Create, query, and manage data sources from the terminal.
Upload static assets like images and PDFs to Notion.
### Manage Notion Workers
Create, deploy, and operate Workers — small TypeScript programs that extend Notion with syncs, tools, and webhooks:
```bash theme={null}
ntn workers new # Scaffold a project
ntn workers deploy # Build and upload
ntn workers list # List deployed workers
```
See the [Workers guide](/workers/get-started/overview) to get started.
### Make API requests
Make authenticated requests to the Notion API with inline JSON construction and shell completion:
```bash theme={null}
ntn api v1/users # GET users
ntn api v1/pages parent[page_id]=abc123 # POST with inline body fields
ntn api v1/pages/abc123 -X PATCH archived:=true # PATCH with typed assignment
```
See the [API requests guide](/cli/guides/api-requests) for the full inline syntax reference.
### Upload files
Upload static assets like images and PDFs to reference from Notion pages:
```bash theme={null}
ntn files create < photo.png
ntn files create --external-url https://example.com/photo.png
ntn files list
```
See the [file uploads guide](/cli/guides/file-uploads) for details.
## Next steps
Alternative install methods and shell completions.
Manage login sessions and credentials.
Full reference for every ntn command.
Create and deploy your first Notion Worker.
# API requests
Source: https://developers.notion.com/cli/guides/api-requests
Make Notion API requests from the terminal.
Use `ntn api` to make authenticated Notion API requests from your terminal. It is useful when you want to inspect an endpoint, test a request body, script an API call, or debug a response without setting up a separate HTTP client.
`ntn api` adds the `Authorization` and `Notion-Version` headers for you. It uses your [CLI authentication](/cli/get-started/authentication) by default, or a token from `NOTION_API_TOKEN` when you set one.
## Make a request
Pass a Notion API path after `ntn api`. The leading slash is optional:
```bash theme={null}
ntn api v1/pages/$PAGE_ID
ntn api /v1/pages/$PAGE_ID
```
Without request body input, `ntn api` sends a `GET` request.
To send a request body, add inline body fields:
```bash theme={null}
ntn api v1/pages \
parent[page_id]="$PARENT_PAGE_ID" \
properties[Name][title][0][text][content]="CLI-created page"
```
When body fields are present, `ntn api` sends a `POST` request unless you override the method.
Use `-X` when the endpoint needs a different method:
```bash theme={null}
ntn api "v1/pages/$PAGE_ID" -X PATCH archived:=true
```
## Build request data inline
Inline inputs after the path can set body fields, query parameters, and request headers.
| Form | Meaning | Example |
| :------------- | :----------------------------- | :------------------------ |
| `path=value` | Body field with a string value | `parent[page_id]=abc123` |
| `path:=json` | Body field parsed as JSON | `archived:=true` |
| `name==value` | Query parameter | `page_size==100` |
| `Header:Value` | Request header | `Accept:application/json` |
Use `=` when the value should be a string:
```bash theme={null}
ntn api v1/search query=roadmap
```
Use `:=` when the value should keep its JSON type:
```bash theme={null}
ntn api "v1/pages/$PAGE_ID" -X PATCH \
archived:=false \
properties[Priority][number]:=2
```
Typed values can be booleans, numbers, strings, arrays, objects, or `null`:
```bash theme={null}
ntn api v1/search \
filter:='{"property":"object","value":"page"}' \
page_size:=10
```
## Choose body syntax
For nested objects, use bracket or dot notation:
```bash theme={null}
ntn api v1/pages \
parent[page_id]="$PARENT_PAGE_ID" \
properties.Name.title[0].text.content="Meeting notes"
```
Use explicit array indexes when order matters:
```bash theme={null}
ntn api "v1/blocks/$PAGE_ID/children" -X PATCH \
children[0][type]=paragraph \
children[0][paragraph][rich_text][0][text][content]="First paragraph" \
children[1][type]=heading_2 \
children[1][heading_2][rich_text][0][text][content]="Next section"
```
Use `[]` to append repeated values in input order:
```bash theme={null}
ntn api v1/comments \
parent[page_id]="$PAGE_ID" \
rich_text[][text][content]="First comment line" \
rich_text[][text][content]="Second comment line"
```
Bracket notation is safest for keys that contain punctuation or spaces:
```bash theme={null}
ntn api "v1/pages/$PAGE_ID" -X PATCH \
properties[Build version][rich_text][0][text][content]="2026.05.11"
```
Inline request syntax is inspired by [HTTPie](https://github.com/httpie/cli)
and implemented in [httpcliparser](https://github.com/jclem/httpcliparser).
## Send JSON from a file or stdin
Use inline inputs for small bodies. Use stdin or `--data` when the body is easier to write as JSON.
Send a JSON file:
```bash theme={null}
ntn api v1/pages < create-page.json
```
Pipe generated JSON:
```bash theme={null}
jq -n --arg page_id "$PARENT_PAGE_ID" '{
parent: { page_id: $page_id },
properties: {
title: {
title: [{ text: { content: "Generated page" } }]
}
}
}' | ntn api v1/pages
```
Pass a JSON string directly:
```bash theme={null}
ntn api v1/search --data '{"query":"roadmap","page_size":10}'
```
Only use one body source per request: stdin JSON, `--data`, or inline body fields. You can still combine headers and query parameters with any one body source.
## Add query parameters and headers
Use `==` for query parameters:
```bash theme={null}
ntn api v1/search query==roadmap page_size==10
```
Use `Header:Value` for request headers:
```bash theme={null}
ntn api v1/users \
Accept:application/json \
X-Trace-Id:cli-test-123
```
Repeated query parameters and headers are preserved in the order you pass them.
`ntn api` already sets `Authorization` and `Notion-Version`. You usually do
not need to pass those headers manually.
## Override the API version
By default, `ntn api` fetches the latest supported `Notion-Version`.
Use `--notion-version` for one request:
```bash theme={null}
ntn api v1/users/me --notion-version 2026-03-11
```
Use `NOTION_API_VERSION` for a shell session or script:
```bash theme={null}
export NOTION_API_VERSION=2026-03-11
ntn api v1/users/me
```
## Inspect endpoints before calling them
List the public API surface:
```bash theme={null}
ntn api ls
```
Print it as JSON for scripts:
```bash theme={null}
ntn api ls --json
```
Show the live endpoint help for a path:
```bash theme={null}
ntn api v1/comments --help
```
Print the reduced OpenAPI fragment for an endpoint:
```bash theme={null}
ntn api v1/comments --spec -X POST
```
Print the official markdown reference page for an endpoint:
```bash theme={null}
ntn api v1/comments --docs -X POST
```
If a path supports multiple methods, pass `-X` so `ntn api` knows which operation to inspect.
## Debug a request
Run with `--verbose` to print request and response metadata to stderr:
```bash theme={null}
ntn --verbose api v1/pages/$PAGE_ID
```
Verbose output includes the final method, URL, request headers, JSON request body, response status, response headers, and response body. The `Authorization` request header is redacted by default.
`--unsafe-verbose` is a hidden debugging flag that disables `Authorization`
header redaction in verbose logs. It is dangerous because it can display
your bearer token in command output. Only use it in a controlled local
environment, and never paste its output into shared logs, tickets, or chat.
## Troubleshooting
| Problem | What to check |
| :------------------------------------------------ | :------------------------------------------------------------------------------------------------- |
| The request used `POST` unexpectedly | Body input, `--data`, or stdin JSON makes `POST` the default. Use `-X` to override it. |
| An inline value has the wrong type | Use `:=` for JSON values like `true`, `10`, `null`, arrays, and objects. Use `=` only for strings. |
| A nested body path is hard to read | Prefer bracket notation, especially for property names with spaces or punctuation. |
| `--spec` or `--docs` says the method is ambiguous | Add `-X GET`, `-X POST`, `-X PATCH`, or the method you want to inspect. |
| The body source conflicts | Use only one of stdin JSON, `--data`, or inline body fields. |
| You need to inspect a failing request | Add `--verbose` and check the final method, URL, status, and `x-request-id`. |
## Next steps
Upload local files or import external files into Notion.
Browse Notion API endpoints and schemas.
# Data sources
Source: https://developers.notion.com/cli/guides/data-sources
Create, query, and manage data sources from the terminal.
A [data source](/reference/data-source) is a table of pages under a Notion database. For background on how databases and data sources fit together, see [Working with databases](/guides/data-apis/working-with-databases).
All examples below assume you've already [authenticated](/cli/get-started/authentication) with `ntn login`.
To work with data sources, you use a combo of `ntn datasources` and `ntn api` commands.
## Find a data source ID
Data sources live under a database. Retrieve the parent database to list its data sources. See [Working with databases](/guides/data-apis/working-with-databases#adding-pages-to-a-data-source) for how to find a database ID from a Notion URL:
```bash theme={null}
ntn api v1/databases/
```
Each item in the response's `data_sources` array contains an `id`. To get the ID from the Notion app: open the database's settings menu, choose **Manage data sources**, click the data source's `•••` menu, and click **Copy data source ID**.
## Retrieve a data source
Fetch the schema (properties, title, parent) for a single data source. See [Retrieve a data source](/reference/retrieve-a-data-source) for the response shape:
```bash theme={null}
ntn api v1/data_sources/
```
## Create a data source
Add a new data source to an existing database. `parent[type]=database_id` is the type discriminator; `parent[database_id]` is the database's unique ID. `properties` is a map of column name to [property schema](/reference/property-schema-object). See [Create a data source](/reference/create-a-data-source) for every supported field:
```bash theme={null}
ntn api v1/data_sources \
parent[type]=database_id \
parent[database_id]= \
title[0][type]=text \
title[0][text][content]="Bugs" \
properties:='{"Name":{"title":{}},"Status":{"select":{"options":[{"name":"Open","color":"red"},{"name":"Closed","color":"green"}]}},"Priority":{"number":{"format":"number"}}}'
```
A default "table" view is created alongside the data source. See [Working with views](/guides/data-apis/working-with-views) to learn about managing views.
## Query a data source
Use `ntn datasources query` to list pages in a data source:
```bash theme={null}
ntn datasources query --limit 50
```
Pass `--filter` with the same shape documented in [Filter data source entries](/reference/filter-data-source-entries):
```bash theme={null}
ntn datasources query --filter '{"property":"Status","select":{"equals":"Open"}}'
```
Paginate with `--start-cursor` using the `next_cursor` from the previous response. Add `--json` for machine-readable output.
For sorts, `filter_properties`, or other options, drop down to `ntn api`. See [Query a data source](/reference/query-a-data-source) for the full request and response:
```bash theme={null}
ntn api v1/data_sources//query \
filter:='{"and":[{"property":"Status","select":{"equals":"Open"}},{"property":"Priority","number":{"greater_than_or_equal_to":2}}]}' \
sorts:='[{"property":"Priority","direction":"descending"}]' \
page_size:=25
```
To speed up large queries, restrict the columns returned with the `filter_properties` query param (it accepts property IDs or names):
```bash theme={null}
ntn api 'v1/data_sources//query?filter_properties[]=title&filter_properties[]=Status'
```
Paginate by passing `start_cursor` from the previous response's `next_cursor` until `has_more` is `false`. Query results cap at 10,000 pages. For larger data sources, narrow with filters or subscribe to [webhooks](/reference/webhooks).
## Update a data source
`PATCH` updates the title, description, parent, or schema. To add a column, pass it under `properties` keyed by name:
```bash theme={null}
ntn api v1/data_sources/ -X PATCH \
title:='[{"type":"text","text":{"content":"Bugs (Q2)"}}]' \
properties:='{"Assignee":{"people":{}}}'
```
To rename or remove a property, key it by its existing name and pass either a new `name` or `null`:
```bash theme={null}
ntn api v1/data_sources/ -X PATCH \
properties:='{"Priority":{"name":"Severity"},"Notes":null}'
```
See [Update a data source](/reference/update-a-data-source) for the full list of editable fields and properties that can't be changed via the API.
## List page templates
List the page templates that show up in a data source's **New** dropdown. See [List data source templates](/reference/list-data-source-templates) for the response shape:
```bash theme={null}
ntn api v1/data_sources//templates
```
Templates are regular Notion pages. Fetch full content with [Retrieve a page](/reference/retrieve-a-page):
```bash theme={null}
ntn api v1/pages/
```
## Discover the endpoints
To see the request schema or jump to the docs for any of these commands:
```bash theme={null}
ntn api v1/data_sources --spec -X POST
ntn api v1/data_sources --docs -X POST
```
# File uploads
Source: https://developers.notion.com/cli/guides/file-uploads
Upload local files or import external files into Notion from the terminal.
Use `ntn files` when you want to create a Notion [File Upload](/reference/file-upload) from your terminal. The command handles the upload lifecycle for local files: it creates the File Upload object, sends the file bytes, completes the upload, and prints the resulting upload ID.
After the upload has a status of `uploaded`, use its ID in a [`file_upload` file object](/reference/block#file) to attach it to a page, block, page icon, page cover, or files property.
## Upload a local file
Redirect the file into `ntn files create`:
```bash theme={null}
ntn files create < ./photo.png
```
By default, the command prints a human-readable summary:
```text theme={null}
ID 43833259-72ae-404e-8441-b6577f3159b4
Filename photo.png
Status uploaded
Content type image/png
Content length 245891
Created time 2026-05-11T20:12:00.000Z
Last edited 2026-05-11T20:12:02.000Z
Expiry time 2026-05-11T21:12:00.000Z
```
For scripts, use `--plain` to return tab-separated fields with the upload ID first:
```bash theme={null}
FILE_UPLOAD_ID=$(ntn files create --plain < ./photo.png | cut -f1)
```
Use `--json` when you want the full File Upload object:
```bash theme={null}
ntn files create --json < ./photo.png
```
`ntn files create` reads bytes from stdin. If you run it without redirecting a file, the command exits with a hint to pass a file with shell redirection.
## Set the filename or content type
The CLI infers the filename and MIME type when it can. Override either value when stdin does not preserve enough information, or when you are generating file bytes from another command:
```bash theme={null}
generate-report --format pdf \
| ntn files create \
--filename weekly-report.pdf \
--content-type application/pdf
```
Use `--filename` when the uploaded file should have a specific name in Notion. Use `--content-type` when the file extension is missing or the inferred MIME type would be ambiguous.
## Import a file from a URL
Use `--external-url` when the file is already hosted at a public HTTPS URL:
```bash theme={null}
FILE_UPLOAD_ID=$(ntn files create --plain \
--external-url https://example.com/photo.png \
--filename photo.png \
| cut -f1)
```
External URL imports are processed asynchronously by Notion. Check the upload status before attaching the file:
```bash theme={null}
ntn files get "$FILE_UPLOAD_ID"
```
Wait until the status is `uploaded`. If the status becomes `failed`, create a new upload after fixing the URL, file type, or file size issue.
## Attach an uploaded file
`ntn files create` uploads the file, but does not choose where to place it in your workspace. Attach the upload by passing the ID to a Notion API endpoint with [`ntn api`](/cli/guides/api-requests).
The examples below use the inline body field syntax described in [API request syntax](/cli/guides/api-requests).
For example, append an image block to a page:
```bash theme={null}
FILE_UPLOAD_ID=$(ntn files create --plain < ./photo.png | cut -f1)
ntn api "v1/blocks/$PAGE_ID/children" -X PATCH \
children[0][type]=image \
children[0][image][type]=file_upload \
children[0][image][file_upload][id]="$FILE_UPLOAD_ID"
```
To attach the upload as a generic file block, change the block type and file field:
```bash theme={null}
FILE_UPLOAD_ID=$(ntn files create --plain < ./contract.pdf | cut -f1)
ntn api "v1/blocks/$PAGE_ID/children" -X PATCH \
children[0][type]=file \
children[0][file][type]=file_upload \
children[0][file][file_upload][id]="$FILE_UPLOAD_ID"
```
To attach the upload to a files property on an existing database page:
```bash theme={null}
FILE_UPLOAD_ID=$(ntn files create --plain < ./contract.pdf | cut -f1)
ntn api "v1/pages/$PAGE_ID" -X PATCH \
properties[Attachments][files][0][type]=file_upload \
properties[Attachments][files][0][file_upload][id]="$FILE_UPLOAD_ID" \
properties[Attachments][files][0][name]=contract.pdf
```
Replace `Attachments` with the name of your files property.
Attach file uploads within one hour. Uploads that are not attached before
their `expiry_time` can expire and cannot be attached later.
## Find an existing upload
List recent file uploads:
```bash theme={null}
ntn files list
```
Retrieve one upload by ID:
```bash theme={null}
ntn files get 43833259-72ae-404e-8441-b6577f3159b4
```
Both commands support `--json` and `--plain` for scripting:
```bash theme={null}
ntn files list --json
ntn files get "$FILE_UPLOAD_ID" --plain
```
`ntn files list` currently returns only the first page. For cursor-based pagination, call the File Uploads API directly:
```bash theme={null}
ntn api v1/file_uploads start_cursor=="$NEXT_CURSOR"
```
## Send a multipart request yourself
Most uploads should use `ntn files create`. Use `ntn api --file` when you need lower-level control over the File Upload API, such as sending one part of an upload that your own script created:
```bash theme={null}
ntn api "v1/file_uploads/$FILE_UPLOAD_ID/send" \
--file ./chunk.bin \
part_number=1
```
`--file` sends a multipart form-data request with the file contents in a form field named `file`. Other inline body fields become additional multipart form fields.
## Troubleshooting
| Problem | What to check |
| :--------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
| `ntn files create` says no bytes were received | Redirect a file into stdin, for example `ntn files create < ./photo.png`. |
| The filename is wrong or missing | Pass `--filename`. This is useful when piping generated bytes. |
| The content type is wrong | Pass `--content-type`, such as `image/png` or `application/pdf`. |
| An external URL import stays `pending` | Poll with `ntn files get ` until it becomes `uploaded` or `failed`. |
| A file upload cannot be attached | Confirm the upload status is `uploaded`, the file type is valid for the target block or property, and the upload has not expired. |
| You need more than the first page of uploads | Use `ntn api v1/file_uploads` with cursor query parameters. |
## Next steps
Build Notion API requests with `ntn api`.
See File Upload statuses, fields, size limits, and endpoints.
# Command reference
Source: https://developers.notion.com/cli/reference/commands
Complete reference for all ntn commands.
## Global flags
Available on every command.
| Flag | Description | Example |
| :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- |
| `-v, --verbose` | Show full error details including source chains. | `ntn workers deploy --verbose` |
| `--workers-config-file ` | Path to a `workers.json` config file (overrides the default lookup in the current directory). The `workspaceId` field in this file selects the workspace for authenticated commands. | `ntn workers deploy --workers-config-file ./prod.workers.json` |
| `-V, --version` | Print version. | `ntn --version` |
| `-h, --help` | Print help. | `ntn workers --help` |
## Environment variables
| Variable | Description |
| :--------------------------- | :------------------------------------------------------------------------------------------- |
| `NOTION_API_TOKEN` | API token for authentication (overrides keychain). |
| `NOTION_KEYRING` | Set to `0` to use file-based auth (`~/.config/notion/auth.json`) instead of the OS keychain. |
| `NOTION_WORKERS_CONFIG_FILE` | Path to `workers.json` (same as `--workers-config-file`). |
| `NOTION_WORKSPACE_ID` | Workspace ID to operate on; skips the workspace prompt. |
## Authentication
| Command | Description | Example |
| :----------- | :-------------------------------------------------- | :----------- |
| `ntn login` | Log in to Notion and connect to a workspace. | `ntn login` |
| `ntn logout` | Clear stored credentials for the current workspace. | `ntn logout` |
## Workers
Commands for managing Notion Workers. Most commands that target a specific worker resolve the worker ID in this order:
1. The `--worker-id` flag (or positional `` argument).
2. The `workerId` field in `workers.json` in the current directory.
If neither is set, the command exits with an error.
### Common flags
Supported across most `workers` subcommands.
| Flag | Description |
| :----------------- | :-------------------------------------------------------------------------------- |
| `--json` | Output as JSON. Mutually exclusive with `--plain`. |
| `--plain` | Output as tab-separated values with no headers. Mutually exclusive with `--json`. |
| `--worker-id ` | Target a specific worker. Defaults to `workers.json`. |
### Commands
| Command | Description |
| :------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ntn workers new [directory]` | Scaffold a new worker project. Prompts for a worker name when `stdin` is a TTY. Flags: `--force` (overwrite conflicting files), `--git`/`--no-git` (force or skip `git init`), `--install`/`--no-install` (force or skip dependency installation). |
| `ntn workers deploy` | Build and upload the worker in the current directory. Creates a new worker if `workers.json` is missing; otherwise updates the existing worker. Flags: `--name ` (required when creating, forbidden when updating), `--local-build` (build locally instead of in the cloud), `--no-git` (walk the filesystem instead of using git). |
| `ntn workers list` | List all workers in the active workspace. Alias: `ls`. No flags beyond common. |
| `ntn workers get [worker-id]` | Show details for a single worker. No flags beyond common. |
| `ntn workers create` | Create a worker without deploying any code. Flags: `--name `. |
| `ntn workers delete [worker-id]` | Delete a worker. Alias: `rm`. Flags: `--yes` (skip confirmation prompt). |
| `ntn workers exec ` | Run a capability (sync, tool, or webhook) and print its output. Flags: `-d/--data ` (JSON input; reads stdin if omitted), `--stream` (stream output as produced), `-l/--local` (run locally via `tsx`), `--dotenv ` (env file for `--local`, default `.env`), `--no-dotenv` (skip loading `.env`). |
| `ntn workers capabilities list` | List all deployed capabilities for a worker. Alias: `ls`. No flags beyond common. |
| `ntn workers tui` | Open the interactive terminal UI for managing workers. Alias: `ui`. No flags. |
## Workers — sync
Manage scheduled syncable capabilities. Each subcommand takes a `` identifying the capability. See the [Syncs guide](/workers/guides/syncs) for usage patterns, pagination, and scheduling.
The [Workers common flags](#common-flags) (`--json`, `--plain`, `--worker-id`) apply to every `sync` subcommand.
| Command | Description |
| :----------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ntn workers sync status [capability-key]` | Show live-updating status for a worker's syncable capabilities. Filters to a single capability when `capability-key` is provided. Flags: `--no-watch` (print once and exit), `--interval ` (poll interval in watch mode, default `2`). |
| `ntn workers sync trigger ` | Trigger a syncable capability to run now, bypassing the schedule. Flags: `--preview` (invoke without writing to the target), `--context ` (cursor from a previous `--preview` run's `nextContext`), `-l/--local` (run locally via `tsx`), `--dotenv ` (env file for `--local`, default `.env`), `--no-dotenv` (skip loading `.env`). |
| `ntn workers sync pause ` | Pause scheduled execution for a sync. In-flight runs are not interrupted. No flags beyond common. |
| `ntn workers sync resume ` | Resume scheduled execution for a previously paused sync. No flags beyond common. |
| `ntn workers sync state get ` | Print the current cursor and stats for a sync. No flags beyond common. |
| `ntn workers sync state reset ` | Clear a sync's cursor and stats so it restarts from scratch on the next run. No flags beyond common. |
## Workers — env
Manage encrypted environment variables for a worker. Values are write-only; they are never returned by `list`. See the [Secrets guide](/workers/guides/secrets) for usage patterns and local development workflows.
| Command | Description |
| :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ntn workers env set ...` | Set one or more environment variables. |
| `ntn workers env list` | List the keys of all environment variables (values are hidden). Alias: `ls`. |
| `ntn workers env unset ` | Remove an environment variable. Aliases: `delete`, `rm`. |
| `ntn workers env pull` | Download remote environment variables to a local `.env` file. Flags: `--file ` (default `.env`), `--no-file` (print to stdout instead of writing), `--yes` (skip confirmation prompt). |
| `ntn workers env push` | Push a local `.env` file to your worker. Flags: `--file ` (default `.env`), `--yes` (skip confirmation prompt). |
## Workers — OAuth
Manage OAuth connections for capabilities that authenticate against external providers. See the [OAuth guide](/workers/guides/oauth) for the full setup flow.
| Command | Description |
| :------------------------------------ | :---------------------------------------------------------------------------------------------------------------------- |
| `ntn workers oauth start ` | Start an OAuth flow for an OAuth capability. Opens the provider's authorization URL. |
| `ntn workers oauth token ` | Print an OAuth access token for a capability. Intended for debugging. With `--plain`, prints just the token for piping. |
| `ntn workers oauth show-redirect-url` | Print the OAuth redirect URL to configure with your provider. |
## Workers — runs
| Command | Description |
| :------------------------------- | :------------------------------------------ |
| `ntn workers runs list` | List recent runs for a worker. Alias: `ls`. |
| `ntn workers runs logs ` | Print logs for a specific run. |
## Workers — webhooks
See the [Webhooks guide](/workers/guides/webhooks) for defining webhook handlers, request verification, and retries.
| Command | Description |
| :-------------------------------------- | :------------------------------------------------------------------ |
| `ntn workers webhooks list [worker-id]` | List webhook URLs for a worker's webhook capabilities. Alias: `ls`. |
## API
| Command | Description |
| :--------------- | :------------------------------------------------------------------------------------------------------ |
| `ntn api ` | Make an authenticated Notion API request. See [API requests](/cli/guides/api-requests) for full syntax. |
| `ntn api ls` | List all available API endpoints. |
## Data sources
### `ntn datasources query `
Query pages in a data source.
Flags:
* `--limit `: page size, default `25`.
* `--start-cursor `: pagination cursor from a previous response.
* `-s, --sort `: ` [asc|desc]`, repeatable.
* `--filter `: raw filter JSON. See [Filter data source entries](/reference/filter-data-source-entries) for the expected shape.
* `--filter-file `: read filter JSON from a file; pass `-` for stdin.
* `--notion-version `: also settable via `NOTION_API_VERSION`.
### `ntn datasources resolve `
Resolve a Notion database ID to its data source IDs.
Flags:
* `--notion-version `: also settable via `NOTION_API_VERSION`.
## Pages
| Command | Description |
| :-------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ntn pages trash ` | Trash a page. Flags: `--yes` (skip confirmation prompt), `--notion-version `. |
| `ntn pages get ` | Retrieve a page as Markdown. Flags: `--json` (output as JSON), `--notion-version `. |
| `ntn pages create` | Create a page from Markdown content. Flags: `--parent ` (parent target: `page:`, `database:`, or `data-source:`), `--content ` (reads stdin if omitted), `--notion-version `. |
| `ntn pages edit ` | Update a page's content from Markdown. Flags: `--content ` (reads stdin if omitted), `--allow-deleting-content` (allow deletion of child pages and databases), `--notion-version `. |
## Files
| Command | Description |
| :-------------------------- | :----------------------- |
| `ntn files create` | Upload a file to Notion. |
| `ntn files get ` | Get upload details. |
| `ntn files list` | List file uploads. |
## Diagnostics
| Command | Description |
| :----------- | :--------------------------------------------------------------------------------------------- |
| `ntn doctor` | Check the health of your Notion CLI setup. Reports on auth, keychain, network, and config. |
| `ntn update` | Update `ntn` to the latest version. Flags: `--force` (reinstall even when already up to date). |
# Audit log events
Source: https://developers.notion.com/compliance/audit-log-events
A comprehensive list of events tracked in the Notion audit log for compliance and security monitoring.
## Event types
Events are split into the following categories:
1. **Page events**: Events users take on a single Notion page.
2. **Data source events**: Events about data sources (databases).
3. **Workspace events**: Events users take on an entire Notion workspace.
4. **Account events**: Events about accounts of users in the workspace.
5. **Teamspace events**: Events users take on one or more teamspaces.
6. **Form events**: Events about forms in the workspace.
7. **Organization events**: Events about organization-level settings.
***
## Page events
* **AI Meeting Notes audio recording downloaded**: That a meeting notes audio recording was downloaded.
* **AI Meeting Notes consent confirmed**: That meeting notes consent was confirmed.
* **Automation created**: That an automation was created.
* **Automation deleted**: That an automation was deleted from a page.
* **Automation edited**: That a user edited an automation.
* **Comment added**: That a discussion comment was created on a page.
* **Comment deleted**: That a discussion comment was deleted from a page.
* **Comment updated**: That a user edited a comment.
* **Email domain permission on page changed**: That a user granted page access to users with a specific email domain.
* **File deleted**: That a file was deleted from a page.
* **File downloaded**: That a user opened or downloaded a file from a certain page.
* **File uploaded**: That a file was uploaded.
* **Page archived**: That a page was archived.
* **Page comments read**: That comments on a page were read.
* **Page created**: That a user created a new page nested under another page.
* **Page deleted from Trash**: That a page was permanently deleted.
* **Page edited**: That a user edited the content of a page.
* **Page exported**: That a user exported a page.
* **Page lock status changed**: That a page's lock status was updated.
* **Page moved**: That a user moved a page.
* **Page moved to Trash**: That a user moved a page to Trash.
* **Page permanently deleted**: That a page was permanently removed from Trash. This can be done by a user, but it can also be done automatically by Notion after 30 days, or within a [custom time frame](https://www.notion.com/help/custom-data-retention-settings) on Enterprise Plans.
* **Page permission updated**: That a member or guest's page permissions were updated.
* **Page properties edited**: That a user edited a page's property, like a page title or a database property.
* **Page restored**: That a user restored a formerly deleted page from Trash.
* **Page shared to web**: That a user enabled sharing (or disabled sharing) a page to the web.
* **Page unarchived**: That a page was unarchived.
* **Page verification status changed**: That a page's verification status was updated.
* **Page viewed**: That a user viewed a page.
* **Page viewed by user on shared email domain**: That a user with an email domain that was granted access viewed a page.
* **Private content transferred**: That the private pages of a user who left the workspace were transferred to a current user. Learn more [here](https://www.notion.so/help/transfer-content-deprovisioned-user).
* **Suggestion accepted**: That a suggestion was accepted on a page.
* **Suggestion added**: That a suggestion was created on a page.
* **Suggestion comment added**: That a comment was created on a suggestion.
* **Suggestion comment deleted**: That a comment was deleted from a suggestion.
* **Suggestion comment updated**: That a user updated a comment on a suggested edit in a page.
* **Suggestion rejected**: That a suggestion was rejected on a page.
* **Transcript deleted from transcription block**: That a transcription block's transcript was deleted.
***
## Data source events
* **Data source created**: That a collection was created.
* **Data source deleted from Trash**: That a collection was permanently deleted.
* **Data source moved**: That a collection was moved.
* **Data source moved to Trash**: That a collection was deleted.
* **Data source page-level access rule updated**: That a collection's permissions were updated.
* **Data source permanently deleted**: That a collection was purged.
* **Data source restored from Trash**: That a collection was restored.
* **Database can create pages permission was updated**: That a database's can create pages permission was updated.
* **Database schema edited**: That a database schema was edited.
***
## Workspace events
* **A managed user was logged out by an admin**: That an admin has logged out a single managed user account.
* **A managed user’s password was cleared by an admin**: That an admin has cleared a single managed user account's password.
* **Ability for managed users to edit their profile information updated**: That the ability for managed users to edit their profile information was updated.
* **Ability for managed users to grant support access updated**: That the ability for managed users to grant support access was updated.
* **Ability for managed users to join external workspaces updated**: That the ability for managed users to join external workspaces was updated.
* **Added allowed email domain**: That a user added an allowed email domain to the workspace.
* **Admin added themselves as integration owner**: That a workspace admin used admin privileges to add themselves as an integration owner.
* **Agent creation settings updated**: That the custom agent creation policy was updated.
* **Agent viewed web URL**: That an AI agent viewed a web URL.
* **AI LEAP (Learning & Early Access Program) toggled**: That the AI LEAP (Learning & Early Access Program) setting was toggled.
* **AI Meeting Notes availability updated**: That the AI meeting notes availability setting was updated.
* **All managed users logged out**: That an admin has logged out every managed user account.
* **All managed users’ passwords cleared**: That an admin has cleared all managed user accounts' passwords.
* **Asana data fetched**: That a Notion admin fetched Asana data on behalf of a workspace user for Asana data importer triage/debugging.
* **Audit Log exported**: That the audit log was exported.
* **Auto-create accounts on sign-in toggled**: That a workspace owner has enabled automatically creating accounts on sign-in.
* **Claimable workspace deletion status change**: That the status of workspace deletion of a claimable workspace has changed.
* **Claimable workspace transfer status change**: That the status of ownership transfer on a claimable workspace has changed.
* **Claimable workspace upgrade status change**: That the status of a claim and upgrade to Enterprise of a claimable workspace has changed.
* **CMK rotation on WEK completed**: That customer-managed key rotation was completed.
* **Confluence data fetched**: That a Notion admin fetched Confluence data on behalf of a workspace user for Confluence data importer triage/debugging.
* **Content Analytics exported**: That a user exported the Content Analytics table of [Workspace Analytics](https://www.notion.so/help/workspace-analytics).
* **Content search queried**: That a workspace owner has used the [content search](https://www.notion.so/help/admin-content-search) functionality to find workspace content.
* **Content search results exported**: That a workspace owner has exported the results from a [content search](https://www.notion.so/help/admin-content-search) query.
* **Custom agent created**: That a custom agent was created.
* **Custom agent creation setting updated**: That a group's custom agent creation setting was updated.
* **Custom agent performed action in setup mode**: That an action was performed in setup mode for a custom agent in the workspace.
* **Custom agent published**: That a custom agent was published.
* **Custom emoji created**: That a custom emoji was created.
* **Custom emoji deleted**: That a custom emoji was deleted.
* **Custom emoji updated**: That a custom emoji was updated.
* **DEK rotation completed**: That data encryption key rotation was completed.
* **DEK rotation started**: That data encryption key rotation was started.
* **Delete from Trash delay updated**: That the [custom data retention](https://www.notion.com/help/custom-data-retention-settings) delete from trash delay setting was updated.
* **Disable guests toggled**: That a workspace owner has enabled or disabled the ability to add guests to a workspace.
* **Disable teamspace guests toggled**: That the disable team guests setting was toggled.
* **Enterprise-managed connection disabled**: That enterprise-managed connections were disabled or removed for the workspace.
* **Enterprise-managed connection enabled**: That enterprise-managed connections were enabled for the workspace.
* **Enterprise-managed connection member access denied**: That a workspace member was denied access to enterprise-managed connections.
* **Enterprise-managed connection member access restored**: That a workspace member's access to enterprise-managed connections was restored.
* **Enterprise-managed connection updated**: That enterprise-managed connection settings were updated for the workspace.
* **Export toggled**: That a workspace owner has disabled or enabled exporting.
* **External AI tool name changed**: That an external agent's display name was changed.
* **External membership requests toggled**: That external membership requests for the workspace were enabled or disabled.
* **External/Public integration connected**: That a public/external integration was connected to the workspace.
* **External/Public integration disconnected**: That a public/external integration was disconnected from the workspace, or a workspace owner removed access to a public integration for all users.
* **Group created**: That a workspace group was created.
* **Group creation setting updated**: That a user enabled or disabled the ability for non-admin members to create groups.
* **Group deleted**: That a workspace group was deleted.
* **Group member role updated**: That a workspace group member's role was updated.
* **Group renamed**: That a workspace group was renamed.
* **Guest invite request created**: That a guest invite request was created.
* **Guest invite request resolved**: That a guest invite to a page was requested for approval and the workspace owner either approved or denied the request.
* **Guest invite requests toggled**: That the guest invite request approval setting was enabled or disabled.
* **Guest membership requests toggled**: That guest membership requests for the workspace were enabled or disabled.
* **Guest removed**: That a guest has been removed from a workspace.
* **HIPAA Compliance updated for workspace.**: That a workspace owner has enabled or disabled HIPAA compliance by accepting or revoking Notion's Business Associate Agreement.
* **IDP metadata URL updated**: That a workspace owner has set or updated the IdP metadata URL.
* **IDP metadata XML removed**: That a workspace owner has removed IdP metadata XML.
* **IDP metadata XML updated**: That a workspace owner has updated the IdP metadata XML.
* **Integration added to approved connections**: That an integration was added to the workspace's list of approved connections.
* **Integration added to workspace**: That a new integration has been added to a workspace.
* **Integration creation settings updated**: That the internal integration creation policy was updated.
* **Integration installation toggled**: That a workspace owner has disabled or enabled integration restrictions.
* **Integration owner added**: That an owner was added to an internal integration.
* **Integration owner removed**: That an owner was removed from an internal integration.
* **Integration page access permissions updated**: That the list of people or groups allowed to manage page access for an integration was updated.
* **Integration permissions updated**: That an integration's capabilities (reading content, inserting a comment, etc.) have been changed.
* **Integration removed from approved connections**: That an integration was removed from the workspace's list of approved connections.
* **Integration removed from workspace**: That an integration has been deleted.
* **Integration secret reset**: That an integration's installation access token has been refreshed.
* **Integration settings updated**: That an integration's basic settings, like its name or icon, have been changed.
* **Integration webhook subscription failing**: That an integration's webhook was inactivated due to repeated delivery failures.
* **Integration webhook subscription restored**: That an integration's webhook was reactivated after being inactive.
* **Internal integration creation setting updated**: That a group's internal integration creation setting was updated.
* **Invite link reset**: That a user has reset an invite link.
* **Invite link toggled**: That a user either enabled or disabled the invite link.
* **MCP allowlist disabled**: That the MCP allowlist was disabled.
* **MCP allowlist enabled**: That the MCP allowlist was enabled.
* **MCP client added to allowlist**: That an MCP client was added to the allowlist.
* **MCP client removed from allowlist**: That an MCP client was removed from the allowlist.
* **MCP server connected**: That an MCP server was connected to the workspace, including via an enterprise-managed connection.
* **Member added to group**: That a workspace owner or membership admin has added a user to a group.
* **Member invited**: That a workspace owner or Membership admin invited a user to the workspace. The new user's role will be specified as `Workspace owner` if they are invited as a workspace owner, or as `Membership admin` if they are invited as a membership admin.
* **Member joined**: That a user has joined the workspace.
* **Member removed**: That a workspace owner or membership admin has removed a user from the workspace.
* **Member removed from group**: That a workspace owner or membership admin has removed a user from a group.
* **Member role updated**: That a workspace owner has updated a user's role.
* **Membership request resolved**: That a user has resolved a workspace membership request.
* **Membership requests toggled**: That a user has enabled or disabled new workspace membership requests.
* **Monday.com data fetched**: That a Notion admin fetched Monday.com data on behalf of a workspace user for Monday.com data importer triage/debugging.
* **Notion AI toggled for workspace.**: That a user has enabled or disabled Notion AI in a workspace.
* **Page access requests toggled**: That a user has enabled or disabled page access requests from non-workspace-members.
* **Pages to other workspaces toggled**: That a workspace owner has either disabled or enabled moving pages to other workspaces.
* **People cards toggled**: That the people hover card setting was toggled.
* **People directory toggled**: That the people directory setting was toggled.
* **Permanently delete delay updated**: That the [custom data retention](https://www.notion.com/help/custom-data-retention-settings) purge delay setting was updated.
* **Personal access token creation setting updated**: That a group's personal access token creation setting was updated.
* **Personal access token creation settings updated**: That the personal access token creation policy was updated.
* **Public domain created**: That a public domain was created for the workspace.
* **Public domain deleted**: That a public domain was deleted from the workspace.
* **Public domain updated**: That a public domain was updated for the workspace.
* **Public home page link cleared**: That a workspace owner has cleared public home page.
* **Public home page set**: That a workspace owner has changed public home page.
* **Public page sharing toggled**: That a workspace owner has switched public page sharing on/off.
* **Removed allowed email domain**: That a user removed an allowed email domain from a workspace.
* **Restricted member invite request resolved**: That a restricted member invite request was resolved.
* **SAML authorization for workspace**: That a user was authorized via SAML.
* **SAML enable setting toggled**: That an organization owner has disabled or enabled SAML.
* **SAML enforce setting toggled**: That an organization owner has disabled or enabled Enforce SAML.
* **SCIM token generated**: That a workspace owner generated a SCIM API token.
* **SCIM token revoked**: That a workspace owner revoked a SCIM API token.
* **Search performed**: That a search was performed in the workspace.
* **Session duration for managed users updated**: That the session duration for managed users was updated.
* **Teamspace content exported**: That a user has exported content from specific teamspaces in the workspace.
* **Temporary member expiry date updated**: That a workspace owner or admin updated a temporary member's expiry date.
* **Toggled ability for users to use ‘Send webhook’ action in automations**: That a workspace owner has enabled or disabled the ability for users to use 'Send webhook' action in automations.
* **User Analytics exported**: That a user exported the User Analytics table of [Workspace Analytics](https://www.notion.so/help/workspace-analytics).
* **WEK generated**: That a workspace encryption key was generated.
* **WEK revoked**: That a workspace encryption key was revoked.
* **Workspace analytics tracking toggled**: That a user enabled or disabled workspace analytics within the workspace.
* **Workspace consolidation completed**: That the source or target workspace has finished consolidation.
* **Workspace consolidation failed**: That workspace consolidation has failed for the source or target workspace.
* **Workspace consolidation started**: That a Notion employee has initiated workspace consolidation from this source or to this target workspace.
* **Workspace content exported**: That a user has exported content from a page or the entire workspace.
* **Workspace creation setting updated**: That a workspace owner has restricted creation of new workspaces by users with the claimed enterprise email domain.
* **Workspace domain changed**: That the domain of a workspace is changed.
* **Workspace icon changed**: That the workspace icon was changed.
* **Workspace members exported**: That workspace members were exported.
* **Workspace name changed**: That a user updated the workspace's name.
* **Workspace sidebar editing toggled**: That a workspace owner has enabled or disabled the ability for users to change the Workspace sidebar.
* **Workspace teams read**: That workspace teams were read.
* **Workspace users read**: That workspace users were read.
***
## Account events
* **Email changed**: That the email of a user was changed.
* **Granted support access**: That a user's account was granted Notion support access.
* **Login**: When and from where a user has logged in.
* **Logout**: That a user logged out.
* **MFA backup code toggled**: That a user updated their MFA backup code settings.
* **MFA SMS toggled**: That a user updated their MFA via SMS text messages settings. Learn more [here](https://www.notion.so/help/two-step-verification).
* **MFA TOTP toggled**: That a user updated their MFA via a TOTP (time-sensitive one time passcode) app. Learn more [here](https://www.notion.so/help/two-step-verification).
* **Name changed**: That a user has updated their account's preferred name.
* **Password changed**: That a user changed their password.
* **Password cleared**: That a user cleared their password.
* **Password set**: That a user created a password.
* **Picture changed**: That the profile photo of the user was changed.
* **Revoked support access**: That a user's account was revoked Notion support access.
* **User alias added**: That a user added an email alias.
* **User alias made primary**: That a user made an email alias primary.
* **User alias removed**: That a user removed an email alias.
* **User analytics tracking toggled**: That a user updated their analytics tracking setting.
* **User deleted**: That a specific user account has been deleted.
* **User reactivated**: That an admin has unsuspended a managed user account.
* **User suspended**: That an admin has suspended a managed user account.
***
## Teamspace events
* **Custom permissions updated for a group in the teamspace**: That a teamspace owner modified access to a group. Learn more [here](https://www.notion.so/help/guides/grant-access-teamspaces).
* **Custom permissions updated for a member in the teamspace**: That a teamspace owner modified access to a teamspace member. Learn more [here](https://www.notion.so/help/guides/grant-access-teamspaces).
* **Enabled teamspaces**: That a user has enabled the teamspaces feature on a workspace.
* **Everyone in workspace default page permission updated**: That the default page permissions of everyone at workspace have been changed.
* **Group added to teamspace**: That a user added a permission group to the teamspace.
* **Group removed from teamspace**: That a teamspace owner has removed a permission group from the teamspace.
* **Member added to teamspace**: That a user added another user to the teamspace. Will specify "as Teamspace owner" if user is invited as a teamspace owner.
* **Member joined teamspace**: That a user joined an open teamspace.
* **Member left teamspace**: That a user left a teamspace.
* **Member removed from teamspace**: That a teamspace owner has removed a teamspace member from the teamspace.
* **Member teamspace role updated**: That a user has updated a teamspace member's role in the teamspace.
* **Teamspace archived**: That a teamspace owner archived a teamspace.
* **Teamspace created**: That a user created the teamspace.
* **Teamspace creation setting toggled**: That a user has enabled or disabled the ability for everyone in the workspace to create a teamspace.
* **Teamspace default toggled**: That a user enabled or disabled a teamspace as a default teamspace.
* **Teamspace description changed**: That the teamspace description has been changed.
* **Teamspace disable guests toggled**: That a teamspace owner has enabled or disabled the ability to add guests to a teamspace.
* **Teamspace export toggled**: That a teamspace owner has disabled or enabled exporting for a teamspace.
* **Teamspace Guests default permission updated**: That the default page permissions of teamspace guests have been changed.
* **Teamspace icon changed**: That the teamspace icon has been changed.
* **Teamspace invite access changed**: That a user has updated settings for who can invite teamspace members.
* **Teamspace Members default permission updated**: That the default page permissions of teamspace members have been changed.
* **Teamspace name changed**: That a user updated the teamspace's name.
* **Teamspace privacy type changed**: That a teamspace owner has changed the teamspace privacy type.
* **Teamspace public page sharing toggled**: That a teamspace owner has switched public page sharing on/off for a teamspace.
* **Teamspace restored**: That a teamspace was restored.
* **Teamspace sidebar editing toggled**: That a teamspace owner has enabled or disabled the ability for users to change the teamspace sidebar section.
***
## Form events
* **Form created**: That a user created a form.
* **Form edited**: That a user updated a form's content.
* **Form permission updated**: That a form's permissions were updated.
* **Form response created**: That a form response was submitted.
* **Form viewed**: That a form was viewed.
***
## Organization events
* **“Allow access to webhooks in database automations and buttons” toggled for the organization**: That the webhook automation action setting was toggled for an organization.
* **“Allow any user to request to be added as a member of the workspace” toggled for the organization**: That the "Allow any user to request to be added as a member of the workspace" setting was toggled for an organization.
* **“Allow members to request adding other members” toggled for the organization**: That the "Allow members to request adding other members" setting was toggled for an organization.
* **“Allow page access requests from non-members” toggled for the organization**: That the "Allow page access requests from non-members" setting was toggled for an organization.
* **“Allow page guests to request to be added as members to the workspace” toggled for the organization**: That the "Allow page guests to request to be added as members to the workspace" setting was toggled for an organization.
* **“Disable export” toggled for the organization**: That the disable export setting was toggled for an organization.
* **”Guest can create private pages” toggled for the organization**: That the guest private page creation setting was toggled for an organization.
* **“People cards” toggled for the organization**: That the people hover card setting was toggled for an organization.
* **“People directory” toggled for the organization**: That the people directory setting was toggled for an organization.
* **“Workspace support access consent” toggled for the organization**: That the workspace support access consent setting was toggled for an organization.
* **A managed user was logged out by an admin**: That an admin has logged out a single managed user account in the organization.
* **A managed user’s password was cleared by an admin**: That an admin has cleared a single managed user account's password in the organization.
* **Ability for managed users to edit their profile information updated**: That the ability for managed users to edit their profile information was updated for an organization.
* **Ability for managed users to grant support access updated**: That the ability for managed users to grant support access was updated for an organization.
* **Ability for managed users to join external workspaces updated**: That the ability for managed users to join external workspaces was updated for an organization.
* **Admin role assigned**: That an admin role was assigned to a member.
* **Admin role revoked**: That an admin role was revoked from a member.
* **AI provider credential created**: That an AI provider credential was created for an organization.
* **AI provider credential deleted**: That an AI provider credential was deleted for an organization.
* **AI provider credential rotated**: That an AI provider credential was rotated for an organization.
* **AI toggled for the organization**: That the AI feature setting was toggled for an organization.
* **All managed users logged out**: That an admin has logged out every managed user account in the organization.
* **All managed users’ passwords cleared**: That an admin has cleared all managed user accounts' passwords in the organization.
* **Auto-create accounts on sign-in toggled**: That automatic account creation on sign-in was toggled for an organization.
* **Custom admin role created**: That a custom admin role was created.
* **Custom admin role deleted**: That a custom admin role was deleted.
* **Custom admin role updated**: That a custom admin role was edited.
* **Default new workspace region updated**: That the workspace region was updated for managed users.
* **Disable duplicating pages to other workspaces toggled for the organization**: That the disable duplicating pages to other workspaces setting was toggled for an organization.
* **Disable guests toggled for the organization**: That the disable guests setting was toggled for an organization.
* **Disable publishing sites and forms toggled for the organization**: That the disable publishing sites and forms setting was toggled for an organization.
* **IDP metadata URL updated**: That the IdP (Identity Provider) metadata URL was updated for an organization.
* **IDP metadata XML removed**: That the IdP (Identity Provider) metadata XML was removed for an organization.
* **IDP metadata XML updated**: That the IdP (Identity Provider) metadata XML was updated for an organization.
* **Integration installation toggled for organization**: That the integration installation restriction was updated for an organization.
* **IP allowlist created**: That an IP restriction was created for an organization.
* **IP allowlist deleted**: That an IP restriction was deleted from an organization.
* **IP allowlist updated**: That an IP restriction was updated for an organization.
* **IP restriction enforcement mode changed**: That the IP restriction enforcement mode was changed for an organization.
* **IP restrictions toggled**: That IP restriction was enabled or disabled for an organization.
* **Legal hold content summary exported**: That a legal hold content summary was exported.
* **Legal hold created for organization**: That a legal hold was created.
* **Legal hold export content created**: That a legal hold export content was created.
* **Legal hold member added**: That a member was added to a legal hold.
* **Legal hold member removed**: That a member was removed from a legal hold.
* **Legal hold name updated for organization**: That a legal hold's name was updated.
* **Legal hold released for organization**: That a legal hold was released.
* **Organization audit log exported**: That an organization's audit log was exported.
* **Organization created**: That an organization was created.
* **Organization name changed**: That an organization's name was changed.
* **Organization owner added**: That an owner was added to an organization.
* **Organization owner removed**: That an owner was removed from an organization.
* **Organization token created**: That an organization token was created.
* **Organization token updated**: That an organization token was updated.
* **Organization unverified an email domain**: That an email domain was unverified for an organization.
* **Organization verified an email domain**: That an email domain was verified for an organization.
* **Organization’s request to claim a domain had its status updated**: That an organization's request to claim a domain had its status updated.
* **Page view analytics toggled for the organization**: That the page view analytics setting was toggled for an organization.
* **Session duration for managed users updated**: That the session duration for managed users was updated for an organization.
* **Toggled enable SAML for all spaces in the organization**: That an organization owner has disabled or enabled SAML for all spaces in the organization.
* **Toggled enforce SAML for all spaces in the organization**: That an organization owner has disabled or enabled Enforce SAML for all spaces in the organization.
* **Toggled require SAML authorization for workspace access**: That the required auth step setting was updated for an organization.
* **Workspace added to organization**: That a workspace was added to an organization.
* **Workspace creation setting updated**: That the workspace creation setting was updated for an organization.
* **Workspace removed from organization**: That a workspace was removed from an organization.
# Overview
Source: https://developers.notion.com/compliance/overview
Enterprise-grade security and compliance features for tracking and auditing workspace activity in Notion.
Notion provides robust compliance and security features for organizations that need visibility into workspace activity. These features help security teams monitor, audit, and respond to events across their Notion deployment.
## Available compliance features
Webhook events sent to your SIEM platform for real-time security monitoring and alerting.
Comprehensive event logs accessible through the Notion admin dashboard for compliance auditing.
These features are available on Notion Enterprise plans. Contact your Notion account team for more information.
# SIEM events
Source: https://developers.notion.com/compliance/siem-events
A comprehensive list of webhook events available in your SIEM platform once you set up the Notion SIEM connection.
Below is a comprehensive list of webhook events that will be available in your SIEM platform once you set up the Notion SIEM connection. All events available in your SIEM platform will correspond to an audit log event. The glossary will help you understand the specific events that are being tracked and how they relate to your organization's security posture. Use this information to fine-tune your dashboards, alerts, and incident management processes.
## Event types
Events are split into the following categories:
1. **Page events**: Events users take on a single Notion page.
2. **Data source events**: Events about data sources (databases). Note: Some data source operations may emit as `page.*` events for historical reasons.
3. **Workspace events**: Events users take on an entire Notion workspace.
4. **Account events**: Events about accounts of users in the workspace.
5. **Teamspace events**: Events users take on one or more teamspaces.
6. **Form events**: Events about forms in the workspace.
## Page audience
For page events, the page audience describes the visibility level of the target page. The audience captured will be one of the following:
* **Private**: The page is not shared with other users.
* **Internal**: The page is shared with other members of the workspace only.
* **External**: The page is shared with one or more guests outside of the workspace and/or with an integration bot.
* **Public**: The page is shared to the web.
***
## SIEM event glossary
### Page events
* **page.archived**: A page was archived.
* **page.button\_automation\_created**: A [button](https://www.notion.so/help/template-buttons) automation was created on a page.
* **page.button\_automation\_updated**: A [button](https://www.notion.so/help/template-buttons) automation was updated on a page.
* **page.comments\_read**: A user viewed comments on a page.
* **page.content\_edited**: The content of an existing page was edited by a user. Page content is also known as a [block](https://www.notion.so/help/what-is-a-block). Content edit events are consolidated into one event every minute while edits are occurring.
* **page.created**: A new page nested under a parent page was created by a user.
* **page.deleted**: A page was deleted by a user. Deleted pages may be restored in the future.
* **page.discussion.comment.created**: A comment on a page was created by a user.
* **page.discussion.comment.deleted**: A comment on a page was deleted by a user.
* **page.discussion.comment.updated**: A comment on a page was edited by a user. Comment edit events are consolidated into one event every minute while edits are occurring.
* **page.exported**: A page was exported to a PDF, HTML, or Markdown file by a user.
* **page.file\_deleted**: A file was deleted from a page by a user.
* **page.file\_downloaded**: A file in a page was downloaded or opened by a user.
* **page.file\_uploaded**: A file was uploaded to a page by a user.
* **page.locked**: A page was locked to prevent further editing.
* **page.meeting\_notes.audio\_recording.downloaded**: A user downloaded an audio recording from an AI Meeting Notes block.
* **page.meeting\_notes.consent.confirmed**: A user confirmed consent to start an AI Meeting Notes transcription.
* **page.moved**: A page was relocated by a user, i.e. the page's parent page updated.
* **page.permanently\_deleted**: A page was permanently deleted from Trash. This can be done by a user, or automatically by Notion after 30 days, or within a [custom time frame](https://www.notion.com/help/custom-data-retention-settings) on Enterprise Plans.
* **page.permissions.group\_role\_added**: A workspace group's page permissions were added, which will allow them to access the page.
* **page.permissions.group\_role\_removed**: A group's page permissions were removed for a page, which will restrict them from having access to the page.
* **page.permissions.group\_role\_updated**: A workspace group's page permissions were updated, changing their type of access.
* **page.permissions.guest\_role\_added**: A guest's page permissions were added, which will allow them to access the page.
* **page.permissions.guest\_role\_removed**: A guest's page permissions were removed, which will restrict them from having access to the page.
* **page.permissions.guest\_role\_updated**: A guest's page permissions were updated, changing their type of access.
* **page.permissions.integration\_role\_added**: A user added an [integration](https://www.notion.so/help/add-and-manage-connections-with-the-api) to a page. Integrations of any type — internal or public/external — will trigger this event.
* **page.permissions.integration\_role\_removed**: A user removed the page permissions for an integration (or "connection"), which will restrict the integration from having access to the page. Integrations of any type — internal or public/external — will trigger this event.
* **page.permissions.integration\_role\_updated**: A user updated the page permissions of an integration (or "connection"). Integrations of any type — internal or public/external — will trigger this event.
* **page.permissions.member\_role\_added**: A member's page permissions were added, which will allow them to access the page.
* **page.permissions.member\_role\_removed**: A member's page permissions were removed, which will restrict them from having access to the page.
* **page.permissions.member\_role\_updated**: A member's page permissions were updated, changing their type of access.
* **page.permissions.shared\_to\_public\_role\_added**: A user enabled sharing a page to the public web.
* **page.permissions.shared\_to\_public\_role\_removed**: A user disabled sharing a page to the public web.
* **page.permissions.shared\_to\_public\_role\_updated**: A user updated the public sharing settings for a page.
* **page.permissions.shared\_with\_email\_domain\_role\_added**: A user granted page access to users with a specific email domain.
* **page.permissions.shared\_with\_email\_domain\_role\_removed**: A user removed page access for users with a specific email domain.
* **page.permissions.space\_role\_added**: Workspace-wide page permissions were added, allowing all workspace members to access the page.
* **page.permissions.space\_role\_removed**: Workspace-wide page permissions were removed, restricting access to the page.
* **page.permissions.space\_role\_updated**: Workspace-wide page permissions were updated, changing the type of access for all workspace members.
* **page.permissions.team\_guest\_role\_added**: A teamspace guest's page permissions were added, allowing them to access the page.
* **page.permissions.team\_guest\_role\_removed**: A teamspace guest's page permissions were removed, restricting them from accessing the page.
* **page.permissions.team\_guest\_role\_updated**: A teamspace guest's page permissions were updated, changing their type of access.
* **page.permissions.team\_owner\_role\_added**: A teamspace owner's page permissions were added, allowing them to access the page.
* **page.permissions.team\_owner\_role\_removed**: A teamspace owner's page permissions were removed, restricting them from accessing the page.
* **page.permissions.team\_owner\_role\_updated**: A teamspace owner's page permissions were updated, changing their type of access.
* **page.permissions.team\_role\_added**: A teamspace's page permissions were added, allowing teamspace members to access the page.
* **page.permissions.team\_role\_removed**: A teamspace's page permissions were removed, restricting teamspace members from accessing the page.
* **page.permissions.team\_role\_updated**: A teamspace's page permissions were updated, changing the type of access for teamspace members.
* **page.properties\_edited**: A user edited a page's property, like a page title or a database property.
* **page.purged**: A page was permanently removed from Trash based on workspace data retention settings.
* **page.recurrence\_automation\_created**: A recurring automation was created on a page.
* **page.recurrence\_automation\_deleted**: A recurring automation was deleted from a page.
* **page.recurrence\_automation\_updated**: A recurring automation was updated on a page.
* **page.restored\_from\_trash**: A user restored a formerly deleted page from Trash.
* **page.suggestion.accepted**: A user accepted a suggested edit on a page.
* **page.suggestion.comment.created**: A user added a comment on a suggested edit.
* **page.suggestion.comment.deleted**: A user deleted a comment on a suggested edit.
* **page.suggestion.comment.updated**: A user updated a comment on a suggested edit.
* **page.suggestion.created**: A user suggested an edit on a page.
* **page.suggestion.rejected**: A user rejected a suggested edit on a page.
* **page.transcription\_block.transcript\_deleted**: A transcript for AI Meeting Notes was permanently deleted based on workspace settings.
* **page.unarchived**: A page was unarchived.
* **page.unlocked**: A page was unlocked to allow editing.
* **page.unverified**: A page's verification was removed.
* **page.verified**: A page was verified.
* **page.viewed**: A user viewed a page.
* **page.viewed.by\_shared\_email\_domain\_user**: A user with an email domain that was granted access to the page viewed it.
### Data source events
* **database.permission.added**: A user added a page-level access rule for a data source in a database.
* **database.permission.removed**: A user removed a page-level access rule for a data source in a database.
* **database.permission.updated**: A user changed a page-level access rule for a data source in a database.
* **database.permissions.can\_create\_pages.group\_disabled**: A group's 'can create pages' permission was disabled for a database.
* **database.permissions.can\_create\_pages.group\_enabled**: A group's 'can create pages' permission was enabled for a database.
* **database.permissions.can\_create\_pages.member\_disabled**: A member's 'can create pages' permission was disabled for a database.
* **database.permissions.can\_create\_pages.member\_enabled**: A member's 'can create pages' permission was enabled for a database.
* **database.permissions.can\_create\_pages.space\_disabled**: Workspace-wide 'can create pages' permission was disabled for a database.
* **database.permissions.can\_create\_pages.space\_enabled**: Workspace-wide 'can create pages' permission was enabled for a database.
* **database.permissions.can\_create\_pages.space\_owner\_disabled**: Workspace owners' 'can create pages' permission was disabled for a database.
* **database.permissions.can\_create\_pages.space\_owner\_enabled**: Workspace owners' 'can create pages' permission was enabled for a database.
* **database.permissions.can\_create\_pages.team\_disabled**: A teamspace's 'can create pages' permission was disabled for a database.
* **database.permissions.can\_create\_pages.team\_enabled**: A teamspace's 'can create pages' permission was enabled for a database.
* **database.permissions.can\_create\_pages.team\_owner\_disabled**: Teamspace owners' 'can create pages' permission was disabled for a database.
* **database.permissions.can\_create\_pages.team\_owner\_enabled**: Teamspace owners' 'can create pages' permission was enabled for a database.
* **database.schema\_edited**: A user edited the schema of a database.
### Workspace events
* **integration.created**: A developer created an internal integration and associated it with the workspace.
* **integration.deleted**: An internal integration associated with the workspace was deleted. Deletions can occur in the My Integrations dashboard, or an admin can remove access to an internal integration for all users.
* **integration.management\_permissions.updated**: The list of people, groups, or workspace owners allowed to manage page access for an internal integration was changed.
* **integration.owner\_added**: A workspace member was added as an explicit owner of an internal integration.
* **integration.owner\_break\_glass\_added**: A workspace admin used break-glass access to add themselves as an owner of an internal integration.
* **integration.owner\_removed**: A workspace member was removed as an explicit owner of an internal integration.
* **integration.permission.updated**: An integration's capabilities (reading content, inserting a comment, etc.) were changed.
* **integration.secret\_reset**: An internal integration's installation access token was reset (or "refreshed").
* **integration.settings.updated**: An integration's basic settings, like its name or icon, were changed.
* **workspace.agent.web\_url\_viewed**: An AI agent viewed a web URL.
* **workspace.audit\_log\_exported**: A workspace owner exported the workspace's audit log.
* **workspace.content\_analytics\_exported**: The Content Analytics table of [Workspace Analytics](https://www.notion.so/help/workspace-analytics) was exported.
* **workspace.content\_exported**: Workspace content for a page or for the entire workspace was exported by a workspace user.
* **workspace.content\_search\_exported**: The results of a [content search](https://www.notion.so/help/admin-content-search) for a workspace was exported by a workspace owner.
* **workspace.content\_search\_queried**: A workspace owner used the [admin content search](https://www.notion.so/help/admin-content-search) functionality to find workspace content. Content searches can retrieve content from public and private pages.
* **workspace.custom\_agent.created**: A custom agent was created in the workspace.
* **workspace.custom\_agent.published**: A custom agent was published in the workspace.
* **workspace.custom\_agent.setup\_mode\_used**: An action was performed in setup mode for a custom agent in the workspace.
* **workspace.custom\_emoji.created**: A custom emoji was created in the workspace.
* **workspace.custom\_emoji.deleted**: A custom emoji was deleted from the workspace.
* **workspace.custom\_emoji.updated**: A custom emoji was updated in the workspace.
* **workspace.domain\_management.claim\_request\_status\_updated**: The status of a claim and upgrade to Enterprise of a claimable workspace was updated.
* **workspace.domain\_management.deletion\_request\_status\_updated**: The status of workspace deletion of a claimable workspace was updated.
* **workspace.domain\_management.transfer\_request\_status\_updated**: A transfer request for a workspace created by a user with a verified domain was updated. See [domain management](https://www.notion.so/help/domain-management) for more information.
* **workspace.ekm.cmk.rotation.completed**: Customer-managed key (CMK) rotation on WEK was completed.
* **workspace.ekm.dek.rotation.completed**: Data encryption key (DEK) rotation was completed.
* **workspace.ekm.dek.rotation.started**: Data encryption key (DEK) rotation was started.
* **workspace.ekm.wek.generated**: A workspace encryption key (WEK) was generated.
* **workspace.ekm.wek.revoked**: A workspace encryption key (WEK) was revoked.
* **workspace.external\_account\_connected**: A public/external integration was connected to the workspace.
* **workspace.external\_account\_disconnected**: A public/external integration was disconnected from the workspace, or a workspace owner removed access to a public integration for all users in the workspace.
* **workspace.group.created**: A new group was created. A group is a defined collection of workspace members.
* **workspace.group.custom\_agent\_creation\_updated**: The custom agent creation permission for a group was enabled or disabled.
* **workspace.group.deleted**: A group was deleted from the workspace.
* **workspace.group.internal\_integration\_creation\_updated**: A permission group's ability to create internal integrations was updated.
* **workspace.group.permissions.member\_added**: A workspace owner or membership admin added a new member to a group. A group is a defined collection of workspace members.
* **workspace.group.permissions.member\_removed**: A workspace owner or membership admin removed a member from a group.
* **workspace.group.permissions.member\_role\_updated**: A group member's role was updated (Member ↔ Group owner).
* **workspace.group.personal\_access\_token\_creation\_updated**: A permission group's ability to create personal access tokens was updated.
* **workspace.group.renamed**: A group name was changed.
* **workspace.guest\_invite\_request\_resolved**: A guest invite to a page was requested for approval and the workspace owner either approved or denied the request.
* **workspace.guest\_invite\_request.created**: A guest invite request was created, requesting to invite a guest to specific pages.
* **workspace.imports.asana\_data\_fetched**: A Notion admin fetched Asana data on behalf of a workspace user for Asana data importer triage/debugging purposes.
* **workspace.imports.confluence\_data\_fetched**: A Notion admin fetched Confluence data on behalf of a workspace user for Confluence data importer triage/debugging purposes.
* **workspace.imports.monday\_data\_fetched**: A Notion admin fetched Monday.com data on behalf of a workspace user for Monday.com data importer triage/debugging purposes.
* **workspace.integration\_added**: An integration was added to the workspace for the first time. This event will only be emitted the first time an integration is added to a workspace.
* **workspace.integration\_removed**: All bots for a specific public integration were removed from the workspace.
* **workspace.integration\_webhook\_inactivated**: An integration's webhook was inactivated due to repeated delivery failures.
* **workspace.integration\_webhook\_reactivated**: An integration's webhook was reactivated after being inactive.
* **workspace.mcp.allowlist\_disabled**: The MCP allowlist was disabled for the workspace.
* **workspace.mcp.allowlist\_enabled**: The MCP allowlist was enabled for the workspace.
* **workspace.mcp.client\_added**: An MCP client was added to the workspace's allowlist.
* **workspace.mcp.client\_removed**: An MCP client was removed from the workspace's allowlist.
* **workspace.mcp.enterprise\_managed\_connection\_disabled**: Enterprise-managed connections were disabled or removed for the workspace.
* **workspace.mcp.enterprise\_managed\_connection\_enabled**: Enterprise-managed connections were enabled for the workspace.
* **workspace.mcp.enterprise\_managed\_connection\_member\_access\_denied**: A workspace member was denied access to enterprise-managed connections.
* **workspace.mcp.enterprise\_managed\_connection\_member\_access\_restored**: A workspace member's access to enterprise-managed connections was restored.
* **workspace.mcp.enterprise\_managed\_connection\_updated**: Enterprise-managed connection settings were updated for the workspace.
* **workspace.mcp.server\_connected**: An MCP server was connected to the workspace, including via an enterprise-managed connection.
* **workspace.members\_exported**: A list of workspace members was exported.
* **workspace.membership\_request\_resolved**: A membership request from a member to add a new person to the workspace was resolved, i.e. the workspace owner either approved or denied the request.
* **workspace.permissions.guest\_removed**: A guest was removed from the workspace by a workspace owner or membership admin.
* **workspace.permissions.member\_added**: A user accepted an invite to join a new workspace and has been added to the member list.
* **workspace.permissions.member\_invited**: A user was invited to a workspace by a workspace owner or membership admin.
* **workspace.permissions.member\_removed**: A member was removed from the workspace by a workspace owner or membership admin.
* **workspace.permissions.member\_role\_updated**: A member's role in a workspace was updated. Roles include member, membership admin, and workspace owner.
* **workspace.private\_content\_transferred**: The private content of a deprovisioned workspace member was transferred to a new location. Enterprise workspace owners can [transfer content](https://www.notion.so/help/transfer-content-deprovisioned-user) from deprovisioned users.
* **workspace.public\_domain.created**: A public domain was created for the workspace.
* **workspace.public\_domain.deleted**: A public domain was deleted from the workspace.
* **workspace.public\_domain.updated**: A public domain was updated for the workspace.
* **workspace.restricted\_member\_invite\_request\_resolved**: A restricted member invite request was approved or denied by a workspace owner.
* **workspace.saml\_authorization**: A user verified workspace access via SAML SSO.
* **workspace.saml\_sso\_idp\_metadata\_url\_added**: The IdP (Identity Provider) metadata URL was added by a workspace owner.
* **workspace.saml\_sso\_idp\_metadata\_url\_removed**: The IdP (Identity Provider) metadata URL was removed by a workspace owner.
* **workspace.saml\_sso\_idp\_metadata\_url\_updated**: The IdP (Identity Provider) metadata URL was updated by a workspace owner.
* **workspace.saml\_sso\_idp\_metadata\_xml\_added**: The IdP (Identity Provider) metadata XML (Extensible Markup Language) was added by a workspace owner.
* **workspace.saml\_sso\_idp\_metadata\_xml\_removed**: The IdP (Identity Provider) metadata XML (Extensible Markup Language) was removed by a workspace owner.
* **workspace.saml\_sso\_idp\_metadata\_xml\_updated**: The IdP (Identity Provider) metadata XML (Extensible Markup Language) was updated by a workspace owner.
* **workspace.scim\_token\_generated**: A workspace owner generated a SCIM API token.
* **workspace.scim\_token\_revoked**: A workspace owner revoked a SCIM API token.
* **workspace.search\_performed**: A search was performed in the workspace.
* **workspace.settings.agent\_creation\_policy\_updated**: The agent creation policy setting was updated.
* **workspace.settings.ai\_leap\_toggled**: A workspace owner enabled or disabled the Notion AI LEAP (Learning & Early Access Program) setting.
* **workspace.settings.ai\_legal\_terms\_setting\_updated**: A user enabled or disabled Notion AI in the workspace by accepting or revoking AI legal terms.
* **workspace.settings.ai\_meeting\_notes\_availability\_updated**: The AI meeting notes availability setting was updated.
* **workspace.settings.allow\_content\_export\_setting\_updated**: A workspace owner enabled or disabled exporting.
* **workspace.settings.allow\_group\_creation\_setting\_updated**: A user enabled or disabled the ability for non-admin members to create groups.
* **workspace.settings.allow\_guests\_setting\_updated**: A workspace owner enabled or disabled the ability to add guests to the workspace.
* **workspace.settings.allow\_public\_page\_sharing\_setting\_updated**: A workspace owner enabled or disabled public page sharing for the workspace.
* **workspace.settings.allow\_teamspace\_creation\_setting\_updated**: A user enabled or disabled the ability for everyone in the workspace to create a teamspace.
* **workspace.settings.allow\_workspace\_creation\_setting\_updated**: A workspace owner restricted creation of new workspaces by users with the claimed enterprise email domain.
* **workspace.settings.analytics\_tracking\_setting\_updated**: A user enabled or disabled workspace analytics tracking within the workspace.
* **workspace.settings.delete\_from\_trash\_delay**: The [custom data retention](https://www.notion.com/help/custom-data-retention-settings) delete from trash delay setting was updated.
* **workspace.settings.disallow\_webhook\_automation\_action\_toggled**: A workspace owner enabled or disabled the use of webhook automation actions in the workspace.
* **workspace.settings.duplicate\_pages\_to\_workspaces\_setting\_updated**: A workspace owner enabled or disabled moving pages to other workspaces.
* **workspace.settings.email\_domain\_added**: A user added an allowed email domain to the workspace.
* **workspace.settings.email\_domain\_removed**: A user removed an allowed email domain from the workspace.
* **workspace.settings.enable\_saml\_sso\_config\_updated**: An organization owner enabled or disabled SAML.
* **workspace.settings.enforce\_saml\_sso\_config\_updated**: An organization owner enabled or disabled Enforce SAML.
* **workspace.settings.guest\_invite\_request\_setting\_updated**: The guest invite request approval setting was enabled or disabled.
* **workspace.settings.guest\_membership\_request\_setting\_updated**: A user enabled or disabled guest membership requests for the workspace.
* **workspace.settings.hipaa\_compliance\_updated**: A workspace owner enabled or disabled HIPAA compliance by accepting or revoking Notion's Business Associate Agreement.
* **workspace.settings.icon\_updated**: The workspace icon was changed.
* **workspace.settings.integration\_restriction\_settings\_updated**: A workspace owner enabled or disabled integration restrictions.
* **workspace.settings.internal\_integration\_creation\_policy\_updated**: The workspace policy controlling who can create internal integrations was updated.
* **workspace.settings.invite\_link\_reset**: A user reset the workspace invite link.
* **workspace.settings.invite\_link\_setting\_updated**: A user enabled or disabled the workspace invite link.
* **workspace.settings.membership\_request\_setting\_updated**: A user enabled or disabled new workspace membership requests.
* **workspace.settings.name\_updated**: A user updated the workspace's name.
* **workspace.settings.page\_access\_request\_setting\_updated**: A user enabled or disabled page access requests from non-workspace-members.
* **workspace.settings.people\_directory\_setting\_updated**: The people directory visibility setting was enabled or disabled.
* **workspace.settings.people\_hover\_cards\_setting\_updated**: The people hover cards setting was enabled or disabled.
* **workspace.settings.personal\_access\_token\_creation\_policy\_updated**: The workspace policy controlling who can create personal access tokens was updated.
* **workspace.settings.public\_homepage\_added**: A workspace owner set a public homepage.
* **workspace.settings.public\_homepage\_removed**: A workspace owner cleared the public homepage.
* **workspace.settings.public\_homepage\_updated**: A workspace owner changed the public homepage.
* **workspace.settings.public\_pages\_domain\_updated**: The domain for publicly shared pages was updated.
* **workspace.settings.purge\_delay**: The [custom data retention](https://www.notion.com/help/custom-data-retention-settings) purge delay setting was updated.
* **workspace.settings.saml\_automatic\_account\_creation\_setting\_updated**: A workspace owner enabled or disabled automatically creating accounts on SAML sign-in.
* **workspace.settings.sidebar\_editing\_setting\_updated**: A workspace owner enabled or disabled the ability for users to change the workspace sidebar.
* **workspace.teams\_read**: A user viewed the list of teamspaces in the workspace.
* **workspace.user\_analytics\_exported**: The User Analytics table of [Workspace Analytics](https://www.notion.so/help/workspace-analytics) was exported.
* **workspace.users\_read**: A user viewed the list of users in the workspace.
### Account events
* **user.deleted**: A user account was deleted. This event will be sent to any workspace with which the account is associated.
* **user.login**: A user logged into an account.
* **user.logout**: A user logged out of an account.
* **user.settings.alias\_added**: A user added an email alias to their account.
* **user.settings.alias\_made\_primary**: A user made an email alias their primary email address.
* **user.settings.alias\_removed**: A user removed an email alias from their account.
* **user.settings.analytics\_tracking\_setting\_updated**: A user updated their analytics tracking setting.
* **user.settings.email\_updated**: The email of a user was changed.
* **user.settings.login\_method.mfa\_backup\_code\_updated**: A user updated their MFA (Multi-Factor Authentication) backup code settings.
* **user.settings.login\_method.mfa\_sms\_updated**: A user updated their MFA (Multi-Factor Authentication) SMS (Short Message Service) settings.
* **user.settings.login\_method.mfa\_totp\_updated**: A user updated their MFA (Multi-Factor Authentication) TOTP (Time-based One-Time Password) settings.
* **user.settings.login\_method.password\_added**: A user added a password to their account for login purposes.
* **user.settings.login\_method.password\_removed**: A user removed a password from their account.
* **user.settings.login\_method.password\_updated**: A user updated their password.
* **user.settings.preferred\_name\_updated**: A user updated their account's preferred name.
* **user.settings.profile\_photo\_updated**: The profile photo of a user was changed.
* **user.settings.support\_access\_granted**: A user's account was granted Notion support access.
* **user.settings.support\_access\_revoked**: A user's account was revoked Notion support access.
* **user.suspended**: An admin suspended a managed user account.
* **user.unsuspended**: An admin unsuspended a managed user account.
### Teamspace events
* **teamspace.archived**: A teamspace owner archived a teamspace.
* **teamspace.created**: A user created a teamspace.
* **teamspace.permissions.custom\_group\_role\_added**: A teamspace owner added custom permissions for a group that is added to the teamspace.
* **teamspace.permissions.custom\_group\_role\_removed**: A teamspace owner removed custom permissions for a group that is added to the teamspace.
* **teamspace.permissions.custom\_group\_role\_updated**: A teamspace owner updated custom permissions for a group that is added to the teamspace.
* **teamspace.permissions.custom\_member\_role\_added**: A teamspace owner added custom page permissions for a specific teamspace member.
* **teamspace.permissions.custom\_member\_role\_removed**: A teamspace owner removed custom page permissions for a specific teamspace member.
* **teamspace.permissions.custom\_member\_role\_updated**: A teamspace owner updated custom page permissions for a specific teamspace member.
* **teamspace.permissions.default\_member\_role\_updated**: The default teamspace page permissions applied to teamspace members was updated.
* **teamspace.permissions.default\_workspace\_role\_added**: A teamspace owner gave page permissions to workspace users in a closed teamspace.
* **teamspace.permissions.default\_workspace\_role\_removed**: A teamspace owner removed page permissions from workspace users in a closed teamspace.
* **teamspace.permissions.default\_workspace\_role\_updated**: A teamspace owner updated the default page permissions for all workspace users in a teamspace.
* **teamspace.permissions.group\_added**: A group was added to a teamspace. A group is a defined collection of users.
* **teamspace.permissions.group\_removed**: A group was removed from the teamspace by a teamspace owner.
* **teamspace.permissions.member\_added**: A user was added to the teamspace. The user either joined an open teamspace or was added by another member. The event payload will specify "as Teamspace owner" if the user was added with teamspace owner privileges.
* **teamspace.permissions.member\_removed**: A teamspace member was removed from the teamspace. Removal can be triggered by a member leaving or being removed by a teamspace owner.
* **teamspace.permissions.member\_role\_updated**: A teamspace member's role was updated. Roles include Teamspace Member and Teamspace Owner.
* **teamspace.restored**: A previously archived teamspace was restored.
* **teamspace.settings.allow\_content\_export\_setting\_updated**: The setting to allow exporting teamspace content was enabled or disabled.
* **teamspace.settings.allow\_guests\_setting\_updated**: A teamspace owner enabled or disabled the ability to add guests (non-members) to a specific teamspace.
* **teamspace.settings.allow\_public\_page\_sharing\_setting\_updated**: The setting to allow publicly sharing a teamspace page was enabled or disabled by a workspace owner.
* **teamspace.settings.allow\_sidebar\_editing\_setting\_updated**: The setting that determines who can edit the sidebar was updated. The setting will indicate if any teamspace member can edit the sidebar or if editing is only available for teamspace owners.
* **teamspace.settings.default\_setting\_updated**: A user enabled or disabled a teamspace as a default teamspace.
* **teamspace.settings.description\_updated**: The teamspace description was updated.
* **teamspace.settings.icon\_updated**: The teamspace icon was changed.
* **teamspace.settings.member\_invitation\_setting\_updated**: A user updated settings for who can invite teamspace members.
* **teamspace.settings.name\_updated**: A user updated the teamspace's name.
* **teamspace.settings.privacy\_type\_updated**: A teamspace owner changed the teamspace privacy type.
### Form events
* **form\_response.created**: A form response was submitted.
* **form.content.updated**: A user updated a form's content.
* **form.created**: A user created a form.
* **form.permissions.shared\_to\_public\_role\_added**: A user enabled public sharing for a form.
* **form.permissions.shared\_to\_public\_role\_removed**: A user disabled public sharing for a form.
* **form.viewed**: A user viewed a form.
# Creating pages from templates
Source: https://developers.notion.com/guides/data-apis/creating-pages-from-templates
Learn how to apply data source templates to pages created in the Notion API.
## Overview
[Database templates](https://www.notion.com/help/database-templates) save time when adding a new page to a data source. Instead of building manually from a blank page, templates accelerate your workflows by providing a blueprint for the page's properties and content.
For example, a bug tracking database can have templates for various types of bugs, like "Urgent Production Bug" and "User Interface (UI) Bug".
The Notion app can be used to create and manage templates, and designate one as the "default" template:
To take advantage of templates when creating pages in the API, the three main steps are:
Use the [List data source templates](/reference/list-data-source-templates) endpoint, or manually navigate to the template in the Notion app and get its ID. Skip this step if you want to apply the data source's "default" template.
Provide a `template[type]` of `default`, or of `template_id` alongside a `template[template_id]`, to the [Create a page](/reference/post-page) API to kick off the process of "duplicating" a template into a new page.
1. Remember to use a `parent[type]` of `data_source_id` and provide a `parent[data_source_id]` when creating a page under a data source.
2. Store the ID of the newly created page in your app's backend storage systems. This will be necessary in the next step, since the returned page is momentarily blank until the template finishes applying.
If your connection needs to perform additional steps once a template has finished applying to a page and it's ready for use, wait for Notion's systems to populate the page content before proceeding.
1. Register an handler for [connection webhooks](/reference/webhooks) that listens to `page.created` and `page.content_updated` events and uses the [Retrieve block children](/reference/get-block-children) API to confirm the page contents are populated.
## Step 1: Identify the template to use
For connections using the Notion API, use the [List data source templates](/reference/list-data-source-templates) endpoint to retrieve a list of template IDs and titles:
```bash cURL example theme={null}
curl --request GET \
--url 'https://api.notion.com/v1/data_sources/b55c9c91-384d-452b-81db-d1ef79372b75/templates' \
-H 'Notion-Version: 2026-03-11' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"''
```
The API response includes a similar set of information as the Notion app displays in the screenshot above, listing up to 100 templates at a time:
```json JSON response example expandable theme={null}
{
"templates": [
{
"id": "a5da15f6-b853-455d-8827-f906fb52db2b",
"name": "New Generic Task",
"is_default": true
},
{
"id": "9cc74169-8dd7-4104-8b36-ed952ac44bd0",
"name": "New UI Task",
"is_default": false
},
{
"id": "f2d298e3-efeb-4401-bf4f-67e7b194694f",
"name": "New Support Task",
"is_default": false
}
],
"has_more": false,
"next_cursor": null
}
```
**Filtering templates by name**
Use the `name` query parameter to filter the results down to only templates that match the provided substring (case-insensitive).
This can be helpful for narrowing down which template you want, especially when working with a data source that has a large number of templates.
The other available query parameters are: `page_size` (1-100) and `start_cursor` (nullable string); these are used for pagination.
Aside from this API endpoint, templates are regular [pages](/reference/page) in Notion, so you can also get the template ID by opening the template, copying the URL, and extracting the ID from it.
For example, if the template looks like `https://notion.com/notion/New-Hire-Onboarding-a07589e357414b3285a8d02beb8fd9dd`, the template `id` is `a07589e357414b3285a8d02beb8fd9dd`.
Determining the ID of a template will be useful in the next step, where we'll create pages using templates.
## Step 2: Create page using a template
By default, [adding pages to a data source](/guides/data-apis/working-with-databases#adding-pages-to-a-data-source) creates them with only the block `children` you provide. In other words, the content has to be built up from scratch manually.
In the [Create a page](/reference/post-page) API, this corresponds to the `template[type] = "none"` parameter. The two other options for `type` allow you to start taking advantage of the power of templates at page creation time:
| `template[type]` | `template[template_id]` | `template[timezone]` | Behavior |
| :------------------ | :---------------------- | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `none` (or omitted) | N/A | N/A | No template. Provided children and properties are immediately applied. |
| `default` | N/A | *(optional)* | Applies the data source's default template to the newly created page. `children` cannot be specified in the create page request. |
| `template_id` | *(ID of a template)* | *(optional)* | Use an ID from the response of [List data source templates](/reference/list-data-source-templates), or copied from a URL, as the `template_id`. Indicates which exact template to apply to the newly created page. ID can be with or without dashes (-). `children` cannot be specified in the create page request. |
When using a template — either the `default` template or a specific `template_id` — the Create Page API request returns immediately with a [Page](/reference/page) object representing a blank page, aside from any initial `properties` (for example, the `title`) set on it. Store the ID of this page in your backend systems if you need it for Step 3 below.
Afterwards, Notion's systems quickly begin applying the chosen template in the background, replacing the page content and merging in the template's properties. Any placeholder values (e.g. "Current time when duplicating template"), are appropriately populated, the same way they would be when a Notion user applies a template in the app. The key difference is that the API bot user (rather than a person) is set as the "created by" user (i.e. author) of the new page.
**Timezone control**: Template variables like `@now` and `@today` resolve using a timezone. By default, public connections and [personal access tokens](/guides/get-started/personal-access-tokens) use the associated user's timezone, and internal connections use UTC. To override this, pass an [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string in `template[timezone]` (e.g. `America/New_York`, `Europe/London`, `Asia/Tokyo`).
**Check page and database permissions**
The Notion API returns an HTTP 400 `validation_error` response in the following scenarios:
* **The provided template ID is invalid**. Template IDs are UUIDs (v4) and look like page IDs in Notion.
* **The connection doesn't have access to the template**. Generally, if a bot is connected to the data source's parent database, those permissions apply to all templates for the data source by default, since templates are represented as (special) pages under the data source.
* However, to confirm, check the "Connections" list under the 3-dot overflow menu for a template to ensure your bot appears in the list.
* **Attempting to apply the default template when there isn't one**. When using `template[type]=default`, ensure the `parent` in the Create Page request is pointing to the correct `data_source_id`, and that the data source has a template that's marked as "default".
## Step 3: Confirm page contents are ready
After Step 2, Notion's systems asynchronously begin processing a task to populate the page contents and properties based on the template you identified. In most cases, this is visually very prompt when viewing the data source in the Notion app, but for API connections, you might need an additional step to wait for processing to complete, in cases where your connection needs to take action once the page is ready.
### Webhook setup
Go through the [Webhooks](/reference/webhooks) guide to set up a webhook URL for your connection, and make sure you're using the newest API version in your webhook settings. Also, make sure you have enabled `page.created` and `page.content_updated` events.
### How aggregated events work
Internally, Notion produces a `page.content_updated` event once the template duplication is complete, but in the API, such events might be aggregated into the base `page.created` event if they take place in a short enough time window, which will generally be the case. As a result, you might not see the `page.created` event immediately after Step 2 (the [Create a page](/reference/post-page) call). In these cases, the `page.created` event will be deferred until the page is ready.
In rare cases, or for complex templates, Notion's processing of the page might take longer. In this case, your connection may receive the `page.created` event for the blank page created from Step 2, and subsequently, a `page.content_updated` event when the page is ready.
For more information on event aggregation, refer to [the detailed event types reference](/reference/webhooks-events-delivery#event-aggregation) .
### Webhook implementation
Putting the above flow together, your webhook handler can implement logic as follows:
* When receiving a `page.created` or `page.content_updated` event with the entity ID matching the page Id created in Step 2 👀:
* If the event is `page.content_updated`, you know the template has finished applying, and can proceed to any further steps your connection needs to take ✅.
* If the event is `page.created`, call the [Retrieve block children](/reference/get-block-children) API using the page ID to check if the page content is a blank array, or if it includes the content you expect from the template 👀.
* If the page contents have been populated, you know the template has finished applying, and can proceed to any further steps your connection needs to take ✅,
* Otherwise, stop processing and wait for a `page.content_updated` event signaling the completion of applying the template ⏳.
## Frequently asked questions
In Step 2, the `template_id` parameter can be set to any page, not necessarily a page officially designated as a "template" in the same data source.
However, in all cases, the API bot must have access to the page being used as a template, and it must be in the same workspace. Using the ID of a page in a different data source is currently not recommended, because the schema may not match, causing some properties to fail to be merged into the destination page.
When using a `type` of `default` instead of `template_id`, the conditions are more strict: the page you're creating must be under a data source that has a template marked as "default". Use the [List data source templates](/reference/list-data-source-templates) API
Yes! The [Update page](/reference/patch-page) API also supports a `template` body parameter, with a `type` of either `default` or `template_id`. You can also pass `template[timezone]` to control the timezone for template variable resolution.
When applying a template to an existing page, the template's content is appended to any existing page content. There's another optional body parameter, `erase_content`, that can be set to `true` if you instead want the template's content to fully replace any existing page content. Use caution with this flag, as this is a destructive operation that cannot be reversed in the API!
If you're using the Notion TypeScript SDK, upgrade to [version 5.3.0](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.3.0) or newer to get access to the APIs described in this guide.
If you're using an API version older than `2025-09-03`, we recommend first following the [Upgrading to Version 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03) guide to upgrade, since v5+ of the SDK goes hand-in-hand with a minimum `Notion-Version` of `2025-09-03`.
Until you upgrade to the latest version of the SDK, the only way to use these APIs is to craft [custom requests](https://github.com/makenotion/notion-sdk-js?tab=readme-ov-file#custom-requests) using `notion.request(...)`, which may result in degraded static type safety.
Yes! Refer to example `intermediate:6` in the [`intro-to-notion-api` example project](https://github.com/makenotion/notion-sdk-js/tree/9ed31fd7b47b8e799d1a66ee2ae19e89841b8194/examples/intro-to-notion-api).
Yes! The [Notion MCP](/guides/mcp/overview) supports templates in the `notion-create-pages` and `notion-update-page` tools.
* Use the `notion-fetch` tool on a database to see available templates listed in `` tags for each data source.
* Pass a `template_id` when creating a page to pre-populate it from the template.
* Use the `apply_template` command in `notion-update-page` to apply a template to an existing page.
# Enhanced markdown format
Source: https://developers.notion.com/guides/data-apis/enhanced-markdown
Reference for the Notion-flavored Markdown format used by the markdown content endpoints.
## Overview
Enhanced markdown (also called "Notion-flavored Markdown") is an extended Markdown format that supports all Notion block and rich text types. It is used by the markdown content endpoints: `POST /v1/pages` (via the `markdown` body param), `GET /v1/pages/:page_id/markdown`, and `PATCH /v1/pages/:page_id/markdown`.
This format extends standard Markdown with XML-like tags and attribute lists to represent Notion-specific features such as callouts, toggles, columns, mentions, and block-level colors.
## Indentation
Use tabs for indentation. Child blocks are indented one tab deeper than their parent.
## Escaping
Use backslashes to escape special characters. The following characters should be escaped outside of code blocks: `\` `*` `~` `` ` `` `$` `[` `]` `<` `>` `{` `}` `|` `^`
Do **not** escape characters inside code blocks. Code block content is literal.
## Block types
### Text
```
Rich text {color="Color"}
Children
```
### Headings
```
# Heading 1 {color="Color"}
## Heading 2 {color="Color"}
### Heading 3 {color="Color"}
#### Heading 4 {color="Color"}
```
Headings do not support children. Headings 5 and 6 are converted to heading 4.
### Lists
```
- Bulleted list item {color="Color"}
Children
1. Numbered list item {color="Color"}
Children
```
List items should contain inline rich text. Other block types render as children of an empty list item.
### To-do
```
- [ ] Unchecked item {color="Color"}
Children
- [x] Checked item {color="Color"}
Children
```
### Quote
```
> Rich text {color="Color"}
Children
```
For multi-line quotes, use ` ` tags within a single `>` line:
```
> Line 1 Line 2 Line 3 {color="Color"}
```
Multiple `>` lines render as separate quote blocks, not a single multi-line quote.
### Toggle
```html theme={null}
Toggle title
Children (must be indented)
```
Toggle headings use the `{toggle="true"}` attribute:
```
# Heading {toggle="true" color="Color"}
Children
```
### Callout
```
Rich text
Children
```
Callouts can contain multiple blocks and nested children, not just inline rich text. Each child block should be indented.
### Code
````
```language
Code content
```
````
Do not escape special characters inside code blocks. Set the language if known. Use ` ```mermaid ` for Mermaid diagrams.
### Equation
```
$$
Equation
$$
```
### Table
```html theme={null}
Cell content
```
All attributes are optional (default to `false`). Color precedence from highest to lowest: cell, row, column. Table cells can only contain rich text.
### Divider
```
---
```
### Empty line
```
```
Must be on its own line. Plain empty lines are stripped out.
### Columns
```html theme={null}
Children
Children
```
### Media blocks
```
 {color="Color"}
```
```html theme={null}
CaptionCaption
```
### Page and database references
```html theme={null}
TitleTitle
```
### Table of contents
```html theme={null}
```
### Synced block
```html theme={null}
Children
Children
```
## Rich text formatting
| Format | Syntax |
| ------------- | ------------------------------------ |
| Bold | `**text**` |
| Italic | `*text*` |
| Strikethrough | `~~text~~` |
| Underline | `text` |
| Inline code | `` `code` `` |
| Link | `[text](URL)` |
| Inline math | `$equation$` |
| Line break | ` ` |
| Color | `text` |
### Mentions
```html theme={null}
User namePage titleDatabase nameData source nameAgent name
```
Self-closing format is also supported: ``.
### Custom emoji
```
:emoji_name:
```
### Citations
```
[^URL]
```
## Colors
### Text colors
`gray`, `brown`, `orange`, `yellow`, `green`, `blue`, `purple`, `pink`, `red`
### Background colors
`gray_bg`, `brown_bg`, `orange_bg`, `yellow_bg`, `green_bg`, `blue_bg`, `purple_bg`, `pink_bg`, `red_bg`
### Usage
* **Block colors**: Add `{color="Color"}` attribute to the first line of any block.
* **Inline text colors**: Use `Rich text`.
## Complete example
A Notion page with a heading, a callout, a to-do list, and a code block renders as:
````
# Project kickoff {color="blue"}
Ship the MVP by **Friday**.
- [x] Write spec
- [ ] Build prototype
- [ ] Collect feedback
```python
def greet(name):
return f"Hello, {name}!"
```
| Status | Owner |
|---|---|
| In progress | Ada |
````
# Importing external files
Source: https://developers.notion.com/guides/data-apis/importing-external-files
Learn how to migrate files from an external URL to Notion.
## Step 1 - Start a file upload
To initiate the process of transferring a temporarily-hosted public file into your Notion workspace, use the [Create a file upload](/reference/create-file) with a `mode` of `"external_url"`, a `filename`, and the `external_url` itself:
```curl cURL theme={null}
curl --request POST \
--url 'https://api.notion.com/v1/file_uploads' \
-H 'Authorization: Bearer ntn_****' \
-H 'Content-Type: application/json' \
-H 'Notion-Version: 2026-03-11' \
--data '{
"mode": "external_url",
"external_url": "https://example.com/image.png",
"filename": "image.png"
}'
```
At this step, Notion will return a `validation_error` (HTTP 400) if any of the following are true:
* The URL is not SSL-enabled, or not publicly accessible.
* The URL doesn’t expose the `Content-Type` header for Notion to verify as part of a quick `HEAD` HTTPS request.
* The `Content-Length` header (size) of the file at the external URL exceeds your workspace’s per-file size limit.
* You don’t provide a valid filename and a supported MIME content type or extension.
## Step 2 - Wait for the import to complete
After Step 1, Notion begins processing the file import asynchronously. To wait for the upload to finish, your connection can do one of the following:
1. **Polling**. Set up your connection to wait a sequence of intervals (e.g. 5, 15, 30, and 45 seconds, or an exponential backoff sequence) after creating the File Upload and poll the [Retrieve a file upload](/reference/retrieve-file-upload) until the `status` changes from `pending` to `uploaded` (or `failed`).
2. **Listen to webhooks**. Notion will send one of the following types of [connection webhook](/reference/webhooks) events:
1. `file_upload.complete`
1. The import is complete, and your connection can proceed to using the FileUpload ID in Step 3.
2. `file_upload.upload_failed`
1. The import failed. This is typically due to:
1. File size is too large for your workspace (per-file limit exceeded).
2. The external service temporarily hosting the file you’re importing is experiencing an outage, timing out, or requires authentication or additional headers at the time Notion’s systems retrieve your file.
3. The file storage service Notion uses is experiencing an outage (rare).
2. Check the `data[file_import_result]` object for error codes and messages to help troubleshoot.
3. Try again later or with a smaller file. You won’t be able to attach the failed File Upload to any blocks.
3. For both success and failure, the `entity` of the webhook payload will contain a `type` of `"file_upload"` and an `id` containing the ID of the FileUpload from Step 1.
The outcome of the file import is recorded on the [File Upload](/reference/file-upload) object. If the import fails, the status changes to `failed`. If it succeeds, the status changes to `uploaded`.
For example, in response to a `file_upload.upload_failed` webhook, your system can read the `data.file_import_result.error` from the webhook response, or use the [Retrieve a file upload](/reference/retrieve-file-upload) API and check the `file_import_result.error` to debug the import failure:
```typescript TypeScript theme={null}
// GET /v1/file_uploads/:file_upload_id
// --- RETURNS -->
{
"object": "file_upload",
// ...
"status": "failed",
"file_import_result": {
"type": "error",
"error": {
"type": "validation_error",
"code": "file_upload_invalid_size",
"message": "The file size is not within the allowed limit of 5 MiB. Please try again with a new file upload.",
"parameter": null,
"status_code": null
},
}
}
```
The `file_import_result` object contains details on the `success` or `error`. In this example, the problem is a file size validation issue that wasn’t caught during Step 1—potentially because the external host did not provide a `Content-Length` header for Notion to validate with a `HEAD` request. The same file size limits of 5 MiB for a free workspace and 5 GiB for a paid workspace apply to external URL mode.
A file upload with a status of `failed` cannot be reused, and a new one must be created.
## Step 3 - Attach the file upload
Using its ID, attach the File Upload (for example, to a block, page, or database) within one hour of creating it to avoid expiry.
# Query large data sources
Source: https://developers.notion.com/guides/data-apis/query-large-data-sources
Read every page in a data source that holds more rows than the per-query result limit.
A single [data source query](/reference/query-a-data-source) returns at most **10,000 results**. That limit is per query, not per data source: a query is defined by its filter and sort, and once one reaches the limit, pagination stops. `has_more` comes back `false` and the response carries a `request_status` that marks the result as incomplete.
This trips up full exports. A loop that just follows `next_cursor` until `has_more` is `false` looks like it finished, but on a 25,000-row database it quietly returns 10,000 rows. This guide shows how to detect the limit and read every row past it.
A runnable version of everything here is in the [query-large-data-sources cookbook example](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/query-large-data-sources).
## Detect when a query was cut off
Every page of a query response can include a `request_status`. When the result set is capped it is:
```json theme={null}
{
"request_status": {
"type": "incomplete",
"incomplete_reason": "query_result_limit_reached"
}
}
```
Check every response page as you paginate. Any page with `request_status.type === "incomplete"` means the whole query result was capped, even if that signal appears before the last page you receive.
```typescript TypeScript theme={null}
const response = await notion.dataSources.query({
data_source_id: dataSourceId,
})
if (response.request_status?.type === "incomplete") {
// The query hit the 10,000-result limit. Some matching pages were not
// returned, and following next_cursor will not reach them.
}
```
## Read every row with created\_time windows
Because the limit is per query, you can change the query to get a fresh budget. Split the data source into windows by `created_time`: sort ascending, and each time a window hits the limit, start a new query from the last row's timestamp.
This gives every window a stable order to walk through.
Follow `next_cursor` until `has_more` is `false`. The limit also ends the window, because it sets `has_more` to `false`.
Filter to `created_time` on or after the last row you saw, and repeat. Rows that share the boundary timestamp appear in both windows, so de-duplicate by row ID.
Use `created_time`, not `last_edited_time`. `created_time` never changes, so windows stay stable. `last_edited_time` shifts as rows are edited, which moves rows between windows and causes gaps or duplicates.
```typescript TypeScript theme={null}
import { Client, isFullPageOrDataSource } from "@notionhq/client"
import type {
QueryDataSourceParameters,
QueryDataSourceResponse,
} from "@notionhq/client"
const notion = new Client({ auth: process.env.NOTION_API_KEY })
type DataSourceRow = QueryDataSourceResponse["results"][number]
type DataSourceFilter = QueryDataSourceParameters["filter"]
async function queryAllRows({
dataSourceId,
filter,
}: {
dataSourceId: string
filter?: DataSourceFilter
}) {
const rowsById = new Map()
let windowStart: string | undefined
for (;;) {
let limitReached = false
let lastCreatedTime: string | undefined
let cursor: string | undefined
// Drain one window. This loop also ends when the limit is hit, because the
// limit sets has_more (and so next_cursor) to false.
do {
const response = await notion.dataSources.query({
data_source_id: dataSourceId,
sorts: [{ timestamp: "created_time", direction: "ascending" }],
filter: buildWindowFilter(filter, windowStart),
start_cursor: cursor,
page_size: 100,
})
for (const row of response.results) {
rowsById.set(row.id, row) // de-duplicates boundary rows
// Pages and child data sources (in wikis) both carry created_time, and
// either can be the last row in a window, so advance on either.
if (isFullPageOrDataSource(row)) lastCreatedTime = row.created_time
}
if (response.request_status?.type === "incomplete") limitReached = true
cursor = response.next_cursor ?? undefined
} while (cursor)
if (!limitReached) break // the last window finished under the limit
if (!lastCreatedTime || lastCreatedTime === windowStart) {
throw new Error(
`More than the per-query limit share created_time ${lastCreatedTime}. ` +
"Add another filter to split this window."
)
}
windowStart = lastCreatedTime
}
return [...rowsById.values()]
}
function buildWindowFilter(
filter: DataSourceFilter,
windowStart: string | undefined
): DataSourceFilter {
if (windowStart === undefined) return filter
const windowFilter = {
timestamp: "created_time",
created_time: { on_or_after: windowStart },
} satisfies DataSourceFilter
if (!filter) return windowFilter
if ("and" in filter) return { and: [...filter.and, windowFilter] }
if ("or" in filter) {
throw new Error("Split top-level or filters into separate windowed queries.")
}
return { and: [filter, windowFilter] }
}
```
The [SDK for JavaScript and TypeScript](https://github.com/makenotion/notion-sdk-js) includes `iterateAllDataSourceRows` and `collectAllDataSourceRows` helpers (v5.23.0 and later) that wrap this pattern, so you can stream or collect a full data source without writing the windowing loop yourself.
## When a window can't be split
Notion stores `created_time` to the minute. If a single minute holds more than 10,000 rows, for example from a bulk import, the window can't advance by time alone, and the loop above throws. Add another filter to narrow each window, such as a status or category your data divides on, so every window stays under the limit.
## When to use a different approach
Windowing reads the whole data source on demand. Two alternatives are often a better fit:
* **Incremental sync.** If you poll a data source on a schedule to catch changes, switch to [connection webhooks](/reference/webhooks). They notify you of changes as they happen, so you never paginate the full data source or hit the limit.
* **Smaller payloads.** If queries are slow rather than truncated, use the `filter_properties` parameter to return only the properties you need. See the [performance recommendations](/reference/query-a-data-source) on the query endpoint.
## Views
The same 10,000-result limit applies to [view queries](/reference/get-view-query-results), but you can't window them. A view query paginates a fixed, already-capped result set and doesn't accept a filter while paginating. To read every row behind a view, query its underlying data source with the approach above. Pass the view's filter into the windowed data source query so you keep the same row set, then apply the view's sort locally after collection if your export needs the exact display order.
A [retrieved view](/reference/retrieve-a-view) tells you which data source it is scoped to, along with the filter and sort it applies. Read those, then run the windowing query against the data source. The windowing query must keep its own `created_time` sort, so don't pass `view.sorts` into `queryAllRows`:
```typescript TypeScript theme={null}
const view = await notion.views.retrieve({ view_id: viewId })
// Dashboard views aren't scoped to a single data source.
if (!view.data_source_id) {
throw new Error("This view is not scoped to a single data source")
}
const rows = await queryAllRows({
dataSourceId: view.data_source_id,
filter: view.filter ?? undefined,
})
// If you need the exact view order, apply view.sorts to rows in your app after
// collection. Don't pass view.sorts to queryAllRows; it must control API sorts.
```
`buildWindowFilter` combines the view filter with the `created_time` lower bound using `and`. If a view has a top-level `or` filter, the sample throws. Run one windowed query per `or` branch and merge rows by ID, or rewrite the view filter as an `and` group, so the window bound does not exceed the API's filter nesting limit.
## Related
* [Query a data source](/reference/query-a-data-source) endpoint reference
* [Pagination](/reference/intro#pagination) conventions
* [query-large-data-sources cookbook example](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/query-large-data-sources)
# Retrieving existing files
Source: https://developers.notion.com/guides/data-apis/retrieving-files
Learn how to get a download link for files in the Notion API.
Files, images, and other media enrich your Notion workspace — from embedded screenshots and PDFs to page covers, icons, and file properties in databases.
The Notion API makes it easy to retrieve existing files, so your connection can read and reference media programmatically.
This guide walks you through how to retrieve files that already exist in your workspace (typically added via the UI).
## 🔍 What are file objects in Notion?
In the Notion API, files are represented as [file objects](/reference/file-object). These can appear in blocks (like images, files, videos), page covers or icons, or as part of a `files` property in a database.
Each file object has a `type`, which is determined by how the file is stored:
* `external`: A public URL to a file hosted elsewhere (e.g., CDN)
* `file`: A file manually uploaded via the Notion UI
* `file_upload`: A file uploaded programmatically via the API (which becomes a `file` after attachment)
You can retrieve these file objects through API endpoints like [Retrieve a page](/reference/retrieve-a-page), [Retrieve block children](/reference/get-block-children), or [Retrieve page property item](/changelog/retrieve-page-property-values). Let's start there.
## Retrieve files in your workspace
Most files already added in your Notion workspace (like uploaded images, PDF blocks, or file properties) are `file` type objects. These include a temporary URL you can use to download the file.
To retrieve files:
### A. From page content
Use the [Retrieve block children](/reference/get-block-children) endpoint to list blocks on a page:
```bash Bash theme={null}
curl --request GET \
--url 'https://api.notion.com/v1/blocks/{block_id}/children' \
--header 'Authorization: Bearer {YOUR_API_KEY}' \
--header 'Notion-Version: 2026-03-11'
```
If the page has image, video, or file blocks, they’ll look like this:
```json JSON theme={null}
{
"type": "file",
"file": {
"url": "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/...",
"expiry_time": "2025-04-24T22:49:22.765Z"
}
}
```
**Note**:
The `url` is a temporary signed link that expires after 1 hour. Re-fetch the page to refresh it.
### B. From database properties
Use the [Retrieve a page](/reference/retrieve-a-page) endpoint to get a database item with file properties:
```bash Bash theme={null}
curl --request GET \
--url 'https://api.notion.com/v1/pages/{page_id}' \
--header 'Authorization: Bearer {YOUR_API_KEY}' \
--header 'Notion-Version: 2026-03-11'
```
The `properties` field will include any file attachments in the `files` type:
```json JSON theme={null}
"Files & media": {
"type": "files",
"files": [
{
"type": "file",
"file": {
"url": "https://s3.us-west-2.amazonaws.com/...",
"expiry_time": "2025-04-24T22:49:22.765Z"
},
"name": "Resume.pdf"
}
]
}
```
**What’s Next**
For files larger than 20 MB, split them up and upload using multi-part mode:
# Uploading larger files
Source: https://developers.notion.com/guides/data-apis/sending-larger-files
Learn how to send files larger than 20 MB in multiple parts.
API bots in paid workspaces can use File Uploads in multi-part mode to upload files up to 5 GB. To do so, follow the steps below.
## Step 1 - Split the file into parts
To send files larger than 20 MB, split them up into segments of 5-20 MB each. On Linux systems, one tool to do this is the [`split` command](https://phoenixnap.com/kb/linux-split). In other toolchains, there are libraries such as [`split-file` for TypeScript](https://github.com/tomvlk/node-split-file) to generate file parts.
```shell Shell theme={null}
# Split `largefile.txt` into 10MB chunks, named as follows:
# split_part_aa, split_part_ab, etc.
split -b 10M ./largefile.txt split_part
```
```typescript TypeScript theme={null}
import * as splitFile from "split-file";
const filename = "movie.MOV";
const inputFile = `${__dirname}/${filename}`;
// Returns an array of file paths in the current
// directory with a format of:
// [
// "movie.MOV.sf-part1",
// "movie.MOV.sf-part2",
// ...
// ]
const outputFilenames = await splitFile.splitFileBySize(
inputFile,
1024 * 1024 * 10, // 10 MB
);
```
**Convention for sizes of file parts**
When sending parts of a file to the Notion API, each file must be ≥ 5 and ≤ 20 (binary) megabytes in size, with the exception of the final part (the one with the highest part number), which can be less than 5 MB. The `split` command respects this convention, but the tools in your tech stack might vary.
**To stay within the range, we recommend using a part size of 10 MB**.
## Step 2 - Start a file upload
This is similar to [Step 1 of uploading small files](/guides/data-apis/uploading-small-files#step-1-create-a-file-upload-object), but with a few additional required parameters.
Pass a `mode` of `"multi_part"` to the [Create a file upload](/reference/create-file) API, along with the `number_of_parts`, and a `filename` with a valid extension or a separate MIME `content_type` parameter that can be used to detect an extension.
```curl cURL theme={null}
curl --request POST \
--url 'https://api.notion.com/v1/file_uploads' \
-H 'Authorization: Bearer ntn_****' \
-H 'Content-Type: application/json' \
-H 'Notion-Version: 2026-03-11' \
--data '{
"mode": "multi_part",
"number_of_parts": 5,
"filename": "image.png"
}'
```
## Step 3 - Send all file parts
Send each file part by using the [Send File Upload API](/reference/upload-file) using the File Upload ID, or the `upload_url` in the response of the [Create a file upload](/reference/create-file) step.
This is similar to [Step 2 of uploading small files](/guides/data-apis/uploading-small-files#step-2-upload-file-contents). However, alongside the `file`, the form data in your request must include a field `part_number` that identifies which part you’re sending.
Your system can send file parts in parallel (up to standard Notion API [rate limits](/reference/request-limits)). Parts can be uploaded in any order, as long as the entire sequence from \{1, …, `number_of_parts`} is successfully sent before calling the [Complete a file upload](/reference/complete-file-upload) API.
## Step 4 - Complete the file upload
Call the [Complete a file upload](/reference/complete-file-upload) API with the ID of the File Upload after all parts are sent.
## Step 5 - Attach the file upload
After completing the File Upload, its status becomes `uploaded` and it can be attached to blocks and other objects the same way as file uploads created with a `mode` of `single_part` (the default setting).
Using its ID, attach the File Upload (for example, to a block, page, or database) within one hour of creating it to avoid expiry.
**Error handling**
The [Send](/reference/upload-file) API validates the total file size against the [workspace's limit](/guides/data-apis/working-with-files-and-media#supported-file-types) at the time of uploading each part. However, because parts can be sent at the same time, the [Complete](/reference/complete-file-upload) step re-validates the combined file size and can also return an HTTP 400 with a code of `validation_error`.
We recommend checking the file's size before creating the File Upload when possible. Otherwise, make sure your connection can handle excessive file size errors returned from both the Send and Complete APIs.
To manually test your connection, command-line tools like `head`, `dd`, and `split` can help generate file contents of a certain size and split them into 10 MB parts.
**What’s Next**
Learn how to simplify migrations and syncs into Notion by automating file uploads from external URLs:
# Uploading small files
Source: https://developers.notion.com/guides/data-apis/uploading-small-files
Learn how to send and attach files up to 20 MB using the Notion API.
The **Direct Upload** method lets you securely upload private files to Notion-managed storage via the API. Once uploaded, these files can be reused and attached to pages, blocks, or database properties.
This guide walks you through the upload lifecycle:
Create a file upload object
Send the file content to Notion
Attach the file to content in your workspace
**Tip**:
Upload once, attach many times. You can reuse the same `file_upload` ID across multiple blocks or pages.
## Step 1 - Create a File Upload object
Before uploading any content, start by creating a [File Upload object](/reference/file-upload). This returns a unique `id` and `upload_url` used to send the file.
**Tip:**
Save the `id` — You’ll need it to upload the file in Step 2 and attach it in Step 3.
### Example requests
This snippet sends a `POST` request to create the upload object.
```curl cURL theme={null}
curl --request POST \
--url 'https://api.notion.com/v1/file_uploads' \
-H 'Authorization: Bearer ntn_****' \
-H 'Content-Type: application/json' \
-H 'Notion-Version: 2026-03-11' \
--data '{}'
```
```python Python theme={null}
import json
import requests
payload = {
"filename": file_name,
"content_type": "image/png"
}
file_create_response = requests.post("https://api.notion.com/v1/file_uploads", json=payload, headers={
"Authorization": f"Bearer {NOTION_KEY}",
"accept": "application/json",
"content-type": "application/json",
"Notion-Version": "2026-03-11"
})
if file_create_response.status_code != 200:
raise Exception(
f"File creation failed with status code {file_create_response.status_code}: {file_create_response.text}"
)
file_upload_id = json.loads(file_create_response.text)['id']
```
### Example Response
```json JSON theme={null}
{
"object": "file_upload",
"id": "a3f9d3e2-1abc-42de-b904-badc0ffee000",
"created_time": "2025-04-09T22:26:00.000Z",
"last_edited_time": "2025-04-09T22:26:00.000Z",
"expiry_time": "2025-04-09T23:26:00.000Z",
"upload_url": "https://api.notion.com/v1/file_uploads/a3f9d3e2-1abc-42de-b904-badc0ffee000/send",
"archived": false,
"status": "pending",
"filename": null,
"content_type": null,
"content_length": null,
"request_id": "b7c1fd7e-2c84-4f55-877e-d3ad7db2ac4b"
}
```
## Step 2 - Upload file contents
Next, use the `upload_url` or File Upload object `id` from Step 1 to send the binary file contents to Notion.
**Tips**:
* The only required field is the file contents under the `file` key.
* Unlike other Notion APIs, the Send File Upload endpoint expects a Content-Type of multipart/form-data, not application/json.
* Include a boundary in the `Content-Type` header \[for the Send File Upload API] as described in [RFC 2388](https://datatracker.ietf.org/doc/html/rfc2388) and [RFC 1341](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). Most HTTP clients (e.g. `fetch`, `ky`) handle this automatically if you include `FormData` with your file and don't pass an explicit `Content-Type` header.
### Example requests
This uploads the file directly from your local system.
```bash cURL theme={null}
curl --request POST \
--url 'https://api.notion.com/v1/file_uploads/a3f9d3e2-1abc-42de-b904-badc0ffee000/send' \
-H 'Authorization: Bearer ntn_****' \
-H 'Notion-Version: 2026-03-11' \
-H 'Content-Type: multipart/form-data' \
-F "file=@path/to-file.gif"
```
```javascript JavaScript expandable theme={null}
// Open a read stream for the file
const fileStream = fs.createReadStream(filePath)
// Create form data with the (named) file contents under the `file` key.
const form = new FormData()
form.append('file', fileStream, {
filename: path.basename(filePath)
})
// HTTP POST to the Send File Upload API.
const response = await fetch(
`https://api.notion.com/v1/file_uploads/${fileUploadId}/send`,
{
method: 'POST',
body: form,
headers: {
'Authorization': `Bearer ${notionToken}`,
'Notion-Version': notionVersion,
}
}
)
// Rescue validation errors. Possible HTTP 400 cases include:
// - content length greater than the 20MB limit
// - FileUpload not in the `pending` status (e.g. `expired`)
// - invalid or unsupported file content type
if (!response.ok) {
const errorBody = await response.text()
console.log('Error response body:', errorBody)
throw new Error(`HTTP error with status: ${response.status}`)
}
const data = await response.json()
// ...
```
```python Python expandable theme={null}
file_name = "test.png"
with open(file_name, "rb") as f:
# Provide the MIME content type of the file as the 3rd argument.
files = {
"file": (file_name, f, "image/png")
}
response = requests.post(
f"https://api.notion.com/v1/file_uploads/{file_upload_id}/send",
headers={
"Authorization": f"Bearer {NOTION_KEY}",
"Notion-Version": "2026-03-11"
},
files=files
)
if response.status_code != 200:
raise Exception(
f"File upload failed with status code {response.status_code}: {response.text}")
```
### Example response
```json JSON theme={null}
{
"object": "file_upload",
"id": "a3f9d3e2-1abc-42de-b904-badc0ffee000",
"created_time": "2025-04-09T22:26:00.000Z",
"last_edited_time": "2025-04-09T22:27:00.000Z",
"expiry_time": "2025-04-09T23:26:00.000Z",
"archived": false,
"status": "uploaded",
"filename": "Really funny.gif",
"content_type": "image/gif",
"content_length": "4435",
"request_id": "91a4ee8c-61f6-4c27-bd41-09aa35299929"
}
```
**Reminder:**
Files must be attached within **1 hour** of upload or they’ll be automatically moved to an `archived` status.
## Step 3 - Attach the file to a page or block
Once the file’s `status` is `uploaded`, it can be attached to any location that supports file objects using the File Upload object `id`.
This step uses standard Notion API endpoints; there’s no special upload-specific API for attaching. Just pass a file object with a type of `file_upload` and include the `id` that you received earlier in Step 1.
You can use the file upload `id` with the following APIs:
[Create a page](/reference/post-page)
* Attach files to a database property with the `files` type
* Include uploaded files in `children` blocks (e.g., file/image blocks inside a new page)
[Update page](/reference/patch-page)
* Update existing `files` properties on a database page
* Set page `icon` or `cover`
[Append block children](/reference/patch-block-children)
* Add a new block to a page — like a file, image, audio, video, or PDF block that uses an uploaded file
[Update a block](/reference/update-a-block)
* Change the file attached to an existing file block (e.g., convert an image with an external URL to one that uses a file uploaded via the API)
### Example: add an image block to a page
This example uses the [Append block children](/reference/patch-block-children) API to create a new image block in a page and attach the uploaded file.
```bash cURL theme={null}
curl --request PATCH \
--url "https://api.notion.com/v1/blocks/$PAGE_OR_BLOCK_ID/children" \
-H "Authorization: Bearer ntn_*****" \
-H 'Content-Type: application/json' \
-H 'Notion-Version: 2026-03-11' \
--data '{
"children": [
{
"type": "image",
"image": {
"caption": [],
"type": "file_upload",
"file_upload": {
"id": "'"$FILE_UPLOAD_ID'""
}
}
}
]
}'
```
```python Python theme={null}
# Append image to desired block (this could be a page,
# or a block within a page)
url = f"https://api.notion.com/v1/blocks/{append_block_id}/children"
payload = {
"children": [
{
"object": "block",
"type": "image",
"image": {
"type": "file_upload",
"file_upload": {
"id": file_upload_id
}
}
}
]
}
response = requests.patch(url, headers={
"Authorization": f"Bearer {NOTION_KEY}",
"accept": "application/json",
"content-type": "application/json",
"Notion-Version": "2026-03-11"
}, data=json.dumps(payload))
if response.status_code != 200:
raise Exception(
f"Block append failed with status code {response.status_code}: {response.text}")
```
### Example: add a file block to a page
example uses the [Append block children](/reference/patch-block-children) API to create a new file block in a page and attach the uploaded file.
```bash cURL expandable theme={null}
curl --request PATCH \
--url "https://api.notion.com/v1/blocks/$PAGE_OR_BLOCK_ID/children" \
-H "Authorization: Bearer ntn_*****" \
-H 'Content-Type: application/json' \
-H 'Notion-Version: 2026-03-11' \
--data '{
"children": [
{
"type": "file",
"file": {
"type": "file_upload",
"file_upload": {
"id": "'"$FILE_UPLOAD_ID"'"
}
}
}
]
}'
```
### Example: attach a file property to a page in a database
This example uses the [Update page](/reference/patch-page) API to ad the uploaded file to a `files` property on a page that lives in a Notion database.
```bash cURL expandable theme={null}
curl --request PATCH \
--url "https://api.notion.com/v1/pages/$PAGE_ID" \
-H 'Authorization: Bearer ntn_****' \
-H 'Content-Type: application/json' \
-H 'Notion-Version: 2026-03-11' \
--data '{
"properties": {
"Attachments": {
"type": "files",
"files": [
{
"type": "file_upload",
"file_upload": { "id": "9a8b7c6d-1e2f-4a3b-9e0f-a1b2c3d4e5f6" },
"name": "logo.png"
}
]
}
}
}'
```
### Example: Set a page cover
This example uses the [Update page](/reference/patch-page) API to add the uploaded file as a page cover.
```bash cURL theme={null}
curl --request PATCH \
--url "https://api.notion.com/v1/pages/$PAGE_ID" \
-H 'Authorization: Bearer ntn_****' \
-H 'Content-Type: application/json' \
-H 'Notion-Version: 2026-03-11' \
--data '{
"cover": {
"type": "file_upload",
"file_upload": {
"id": "'"$FILE_UPLOAD_ID"'"
}
}
}'
```
**You’ve successfully uploaded and attached a file using Notion’s Direct Upload method.**
## File lifecycle and reuse
When a file is first uploaded, it has an `expiry_time`, one hour from the time of creation, during which it must be attached.
Once attached to any page, block, or database in your workspace:
* The `expiry_time` is removed.
* The file becomes a permanent part of your workspace.
* The `status` remains `uploaded`.
Even if the original content is deleted, the `file_upload` ID remains valid and can be reused to attach the file again.
Currently, there is no way to delete or revoke a file upload after it has been created.
## Downloading an uploaded file
Attaching a file upload gives you access to a temporary download URL via the Notion API.
These URLs expire after 1 hour.
To refresh access, re-fetch the page, block, or database where the file is attached.
**Tip:**
A file becomes persistent and reusable after the first successful attachment — no need to re-upload.
## Tips and troubleshooting
* **URL expiration**: Notion-hosted files expire after 1 hour. Always re-fetch file objects to refresh links.
* **Attachment deadline**: Files must be attached within 1 hour of upload, or they’ll expire.
* **Size limit**: This guide only supports files up to 20 MB. Larger files require a [multi-part upload](/guides/data-apis/sending-larger-files).
* **Block type compatibility**: Files can be attached to image, file, video, audio, or pdf blocks — and to `files` properties on pages.
**What’s Next**
Now that you know how to upload a file, let’s walk through how to retrieve a file via the API:
# Working with comments
Source: https://developers.notion.com/guides/data-apis/working-with-comments
Learn how to add and retrieve comments with the Notion API.
## Overview
Notion offers the ability for developers to add [comments](https://www.notion.com/help/comments-mentions-and-reminders) to pages and page content (i.e. [blocks](/guides/data-apis/working-with-page-content#modeling-content-as-blocks)) within a workspace. Users may add comments:
* To the top of a page.
* Inline to text or other [blocks](/guides/data-apis/working-with-page-content#modeling-content-as-blocks) within a page.
When using the public API, inline comments can be used to respond to *existing* [discussions](#responding-to-a-discussion-thread).
This guide will review how to use the public REST API to add and retrieve comments on a page. It will also look at considerations specific to [connections](https://www.notion.com/help/add-and-manage-connections-with-the-api) when retrieving or adding comments.
### Permissions
Before discussing how to use the public REST API to interact with comments, let’s first review who can comment on a page. Notion relies on a tiered system for [page permissions](https://www.notion.com/help/sharing-and-permissions#permission-levels), which can vary between:
* `Can view`
* `Can comment`
* `Can edit`
* `Full access`
When using the Notion UI, users must have `Can comment` access or higher (i.e. less restricted) to add comments to a page.
[Connections](/guides/get-started/overview#what-is-a-notion-connection) must also have comment permissions, which can be set in the Developer portal.
Connections are apps developers build to use the public API within a Notion workspace. Connections must be given explicit permissions to read/write content in a workspace, included content related to comments.
### Connection comments capabilities
To give your connection permission to interact with comments via the public REST API, configure the connection to have comment capabilities.
There are two relevant capabilities when it comes to comments — the ability to:
1. Read comments.
2. Write (or insert) comments.
Edit your connection's capabilities in the Developer portal. If these capabilities are not added to your connection, REST API requests related to comments will respond with an error.
See our reference guide on [Capabilities](/reference/capabilities) for more information.
## Comments in Notion’s UI vs. using the REST API
In the Notion UI, users can:
* Add a comment to a page.
* Add an inline comment to child blocks on the page (i.e. comment on page content).
* Respond to an inline comment (i.e. add a comment to an existing discussion thread).
* Read open comments on a page or block.
* Read/re-open resolved comments on a page or block.
* Edit comments.
✅ Using the public REST API, connections **can**:
* Add a comment to a page.
* Update an existing comment.
* Delete a comment.
* Respond to an inline comment (i.e. add a comment to an existing discussion thread).
* Read open comments on a block or page.
❌ When using the public REST API, connections **cannot**:
* Start a new discussion thread.
* Retrieve resolved comments.
Keep an eye on our [Changelog](/page/changelog) for new features and updates to the REST API.
## Retrieving comments for a page or block
The [Retrieve comments](/reference/list-comments) endpoint can be used to list all open (or “un-resolved”) comments for a page or block. Whether you’re retrieving comments for a page or block, the `block_id` query parameter is used. This is because [pages are technically blocks](/guides/data-apis/working-with-page-content).
This endpoint returns a flatlist of comments associated with the ID provided; however, some block types may support multiple discussion threads. This means there may be multiple discussion threads included in the response. When this is the case, comments from all discussion threads will be returned in ascending chronological order. The threads can be distinguished by sorting them `discussion_id` field on each comment object.
```curl cURL theme={null}
curl 'https://api.notion.com/v1/comments?block_id=5c6a28216bb14a7eb6e1c50111515c3d'\
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const blockId = 'd40e767c-d7af-4b18-a86d-55c61f1e39a4';
const response = await notion.comments.list({ block_id: blockId });
console.log(response);
})();
```
By default, the response from this endpoint returns a maximum of 100 items. To retrieve additional items, use [pagination](/reference/intro#pagination).
## Adding a comment to a page
You can add a top-level comment to a page by using the [Create comment](/reference/create-a-comment) endpoint. Requests made to this endpoint require the ID for the parent page, as well as a comment body provided as either [rich text](/reference/rich-text) or a Markdown string with inline formatting support.
The `rich_text` and `markdown` parameters are mutually exclusive — exactly one must be provided per request.
```bash Shell (rich_text) theme={null}
curl -X POST https://api.notion.com/v1/comments \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '
{
"parent": {
"page_id": "59e3eb41-33b2-4151-b05b-31115a15e1c2"
},
"rich_text": [
{
"text": {
"content": "Hello from my connection."
}
}
]
}
'
```
```bash Shell (markdown) theme={null}
curl -X POST https://api.notion.com/v1/comments \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '
{
"parent": {
"page_id": "59e3eb41-33b2-4151-b05b-31115a15e1c2"
},
"markdown": "Hello from my connection. Here is **bold** and *italic* text."
}
'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const response = await notion.comments.create({
parent: {
page_id: "59e3eb41-33b2-4151-b05b-31115a15e1c2"
},
rich_text: [
{
text: {
content: "Hello from my connection.",
},
},
],
});
console.log(response);
})();
```
The `markdown` parameter is a convenient alternative to `rich_text` for agents and scripts that work with Markdown natively. It supports:
* Inline formatting: bold (`**text**`), italic (`*text*`), strikethrough (`~~text~~`), inline code (`` `text` ``), and links (`[text](url)`)
* Inline equations: `$x^2$` or `$$E = mc^2$$`
* User mentions: `name`
* Page mentions: `title`
* Database mentions: `title`
* Date mentions: `` or ``
Block-level Markdown such as fenced code blocks, headings, lists, tables, and blockquotes does not render as structured blocks in comments.
The response will contain the new [comment object](/reference/comment-object).
The exception to what will be returned occurs if your connection has “write comment” capabilities but not “read comment” capabilities. In this situation, the response will be a partial object consisting of only the `id` and `object` fields. This is because the connection can create new comments but can’t retrieve comments, even if the retrieval is just the response for the newly created one. (Reminder: Update the read/write settings in the Developer portal.)
In the Notion UI, this new comment will be displayed on the page using your connection's name and icon.
## Adding an inline comment
To add a block-level inline comment, use the [Create comment](/reference/create-a-comment) endpoint with `parent.block_id`. This creates a comment attached to the whole block, such as a paragraph, heading, or to-do item.
```bash Shell theme={null}
curl -X POST https://api.notion.com/v1/comments \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '
{
"parent": {
"block_id": "d40e767c-d7af-4b18-a86d-55c61f1e39a4"
},
"rich_text": [
{
"text": {
"content": "This comment is attached to a block."
}
}
]
}
'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const response = await notion.comments.create({
parent: {
block_id: "d40e767c-d7af-4b18-a86d-55c61f1e39a4"
},
rich_text: [
{
text: {
content: "This comment is attached to a block.",
},
},
],
});
console.log(response);
})();
```
The public API does not support creating a new discussion anchored to a selected range of text inside a block. To reply to an existing selected-text discussion, use the `discussion_id` for that discussion thread.
## Updating a comment
You can update the content of an existing comment using the [Update comment](/reference/update-a-comment) endpoint. The request requires the `comment_id` of the comment to update and a new body provided as either [rich text](/reference/rich-text) or a Markdown string.
The `rich_text` and `markdown` parameters are mutually exclusive — exactly one must be provided per request.
```bash Shell (rich_text) theme={null}
curl -X PATCH https://api.notion.com/v1/comments/ce18f8c6-ef2a-427f-b416-43531fc7c117 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '
{
"rich_text": [
{
"text": {
"content": "Updated comment text."
}
}
]
}
'
```
```bash Shell (markdown) theme={null}
curl -X PATCH https://api.notion.com/v1/comments/ce18f8c6-ef2a-427f-b416-43531fc7c117 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '
{
"markdown": "Updated comment with **bold** and *italic* text."
}
'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const response = await notion.comments.update({
comment_id: "ce18f8c6-ef2a-427f-b416-43531fc7c117",
rich_text: [
{
text: {
content: "Updated comment text.",
},
},
],
});
console.log(response);
})();
```
The response will contain the updated [comment object](/reference/comment-object).
## Deleting a comment
You can delete a comment using the [Delete comment](/reference/delete-a-comment) endpoint. The request requires the `comment_id` of the comment to delete.
A connection can only delete comments that it created. If the discussion thread is left empty after deleting the last comment, the discussion itself is also removed.
```bash Shell theme={null}
curl -X DELETE https://api.notion.com/v1/comments/ce18f8c6-ef2a-427f-b416-43531fc7c117 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const response = await notion.comments.delete({
comment_id: "ce18f8c6-ef2a-427f-b416-43531fc7c117",
});
console.log(response);
})();
```
The response will contain the deleted [comment object](/reference/comment-object).
## Inline comments
### Responding to a discussion thread
The [Create comment](/reference/create-a-comment) endpoint can also be used to respond to an existing discussion thread on a page or block. (Reminder: Page blocks are the child elements that make up the page content, like a paragraph, header, to-do list, etc.)
If using this endpoint to respond to a discussion, provide a `discussion_id` parameter *instead of* a `parent` object.
Use `parent.block_id` to create a comment attached to a whole block. The API does not support creating a new discussion anchored to selected text within a block; it can only reply to an existing selected-text discussion with `discussion_id`.
#### Retrieving a discussion ID
There are two possible ways to get the `discussion_id` for a discussion thread.
1. You can use the [Retrieve comments](/reference/list-comments) endpoint, which will return a list of open comments on the page or block.
2. You can also get a `discussion_id` manually by navigating to the page with the discussion you’re responding to. Next, click the "Copy link to discussion" menu option next to the discussion.
This will give you a URL like:
```bash theme={null}
https://notion.com/Something-something-a8d5215b89ae464b821ae2e2916ab9ce?d=5e73b63447c2428fa899e906b1f1d20e#b3e87b2b5e114cbd99f96288c22bacce
```
The value of the `d` query parameter is the `discussion_id`.
Once you have the `discussion_id`, you can make a request to respond to the thread like so:
```bash cURL (rich_text) theme={null}
curl -X POST https://api.notion.com/v1/comments \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '
{
"discussion_id": "59e3eb41-33b2-4151-b05b-31115a15e1c2",
"rich_text": [
{
"text": {
"content": "Hello from my connection."
}
}
]
}
'
```
```bash cURL (markdown) theme={null}
curl -X POST https://api.notion.com/v1/comments \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '
{
"discussion_id": "59e3eb41-33b2-4151-b05b-31115a15e1c2",
"markdown": "Hello from my connection."
}
'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const response = await notion.comments.create({
"discussion_id": "8fa6e3ecbebf494b94bae5e9737842fb"
"rich_text": [
{
"text": {
"content": "Hello world"
}
}
]
});
console.log(response);
})();
```
## Conclusion
In this guide, you learned about comment permissions and how to interact with page and block-level comments using Notion’s public REST API. There are many potential use-cases for this type of interaction, such as:
* Commenting on a task when a related pull request is merged.
* Periodically pasting reminders to any pages that meet a certain criteria. For example, you could use the [Query a data source](/reference/query-a-data-source) endpoint to search for a certain criteria and add a comment to any pages that do.
* For apps that use Notion as a CMS (Content Management System) — like a blog — users can give feedback to pages by adding a comment.
## Next steps
* Check out the [API reference documentation](/reference/comment-object) for the comments API.
* Update your version of the Notion JavaScript SDK to make use of this API: `npm install @notionhq/client@latest`.
* Clone our [notion-sdk-typescript-starter](https://github.com/makenotion/notion-sdk-typescript-starter) template repository for an easy way to get started using the API with [TypeScript](https://typescriptlang.org/).
# Working with databases
Source: https://developers.notion.com/guides/data-apis/working-with-databases
Learn about database schemas, querying databases, and more.
## Overview
[Databases](https://www.notion.com/help/intro-to-databases) are collections of [pages](/reference/page) in a Notion workspace that can be filtered, sorted, and organized as needed. They allow users to create and manipulate structured data in Notion.
Connections can be used to help users sync databases with external systems or build workflows around Notion databases.
In this guide, you'll learn:
### Additional types of databases
In addition to regular Notion databases, there are two other types of databases to be aware of. *Neither of these database types are currently supported by the Public API.*
#### Linked databases
Notion offers [linked databases](https://www.notion.com/help/guides/using-linked-databases) as a way of showing databases in multiple places. You can identify them by a ↗ next to the database title which, when clicked, takes you to the source database.
Notion's API does not currently support linked data sources. When sharing a database with your connection, make sure it contains the original data source!
#### Wiki databases
Wiki databases are a special category of databases that allow [Workspace Owners](https://www.notion.com/help/add-members-admins-guests-and-groups) to organize child pages and databases with a homepage view. Wiki database pages can be verified by the Workspace Owner with an optional expiration date for the verification.
Pages in a wiki database will have a [`verification`](/reference/page-property-values#verification) property that can be set through your Notion workspace. See directions for [creating wikis](https://www.notion.com/help/wikis-and-verified-pages#create-a-wiki) and [verifying pages](https://www.notion.com/help/wikis-and-verified-pages#verifying-pages) in our Help Center.
Wiki databases can currently only be created through your Notion workspace directly (i.e., not Notion's API). Ability to retrieve wiki databases in the API may be limited, and you can't add multiple data sources to a wiki database.
To learn more about creating and working with wiki databases, see the following Help Center articles:
## Structure
Database objects, and their data source children, describe a part of what a user sees in Notion when they open a database. See our [documentation on database objects](/reference/database), [data source objects](/reference/data-source), and [data source properties](/reference/property-object) for a complete description.
Databases contain a list of data sources (IDs and names). In turn, each data source can be retrieved and managed separately and acts as the parent for pages (rows of data) that live under them.
```json Database object example expandable theme={null}
{
"object": "database",
"id": "248104cd-477e-80fd-b757-e945d38000bd",
"title": [
{
"type": "text",
"text": {
"content": "Grocery DB",
// ...
},
// ...
}
],
"parent": {
"type": "page_id",
"page_id": "255104cd-477e-808c-b279-d39ab803a7d2"
},
"is_inline": false,
"in_trash": false,
"created_time": "2025-08-07T10:11:07.504-07:00",
"last_edited_time": "2025-08-10T15:53:11.386-07:00",
"data_sources": [
{
"id": "248104cd-477e-80af-bc30-000bd28de8f9",
"name": "Grocery list"
}
],
"url": "https://www.notion.com/example/248104cd477e80fdb757e945d38000bd",
"icon": null,
"cover": {
"type": "external",
"external": {
"url": "https://website.domain/images/image.png"
}
},
}
```
```json Data source object example expandable theme={null}
{
"object": "data_source",
"id": "248104cd-477e-80af-bc30-000bd28de8f9",
"created_time": "2021-07-08T23:50:00.000Z",
"last_edited_time": "2021-07-08T23:50:00.000Z",
"properties": {
"Grocery item": {
"id": "fy%3A%7B", // URL-decoded: fy:{
"type": "title",
"title": {}
},
"Price": {
"id": "dia%5B", // URL-decoded: dia[
"type": "number",
"number": {
"format": "dollar"
}
},
"Last ordered": {
"id": "%5D%5C%5CR%5B", // URL-decoded: ]\\R[
"type": "date",
"date": {}
},
},
"parent": {
"type": "database_id",
"database_id": "248104cd-477e-80fd-b757-e945d38000bd"
},
"database_parent": {
"type": "page_id",
"page_id": "255104cd-477e-808c-b279-d39ab803a7d2"
},
"in_trash": false,
"icon": {
"type": "emoji",
"emoji": "🎉"
},
"title": [
{
"type": "text",
"text": {
"content": "Grocery list",
"link": null
},
// ...
}
]
}
```
The most important part is the data source's schema, defined in the `properties` object.
**Terminology**
The **columns** of a Notion data source are referred to as its “**properties**” or “**schema**”.
The **rows** of a data source are individual [Page](/reference/page)s that live under it and each contain page properties (keys and values that conform to the data source's schema) and content (what you see in the body of the page in the Notion app).
**Schema limitations**
Notion recommends a max property count of **500** or a max schema size of **50KB**. Updates to database schemas that are too large will be blocked to help maintain database performance.
### Database properties
Let's assume you're viewing a database as a table. The columns of the database are represented in the API by database [property objects](/reference/property-object). Property objects store a description of a column, including a type for all the values in a column.
You might recognize a few of the common types:
For each type, additional configuration may also be available. Let's take a look at the `properties` section of an example data source object.
```js Data Source object snippet theme={null}
{
"object": "data_source",
"properties": {
"Grocery item": {
"id": "fy%3A%7B", // URL-decoded: fy:{
"type": "title",
"title": {}
},
"Price": {
"id": "dia%5B", // URL-decoded: dia[
"type": "number",
"number": {
"format": "dollar"
}
},
"Last ordered": {
"id": "%5D%5C%5CR%5B", // URL-decoded: ]\\R[
"type": "date",
"date": {}
},
}
// ... remaining fields omitted
}
```
In this database object, there are three `properties` defined. Each key is the property name and each value is a property object. Here are some key takeaways:
* **The [`"title"`](/reference/property-object#title) type is special.** Every database has exactly one property with the `"title"` type. Properties of this type refer to the page title for each item in the database. In this example, the *Grocery item* property has this type.
* **The value of `type` corresponds to another key in the property object.** Each property object has a nested property named the same as its `type` value. For example, *Last ordered* has the type `"date"`, and it also has a `date` property. **This pattern is used throughout the Notion API on many objects and we call it type-specific data.**
* **Certain property object types have additional configuration.** In this example, *Price* has the type `"number"`. [Number property objects](/reference/property-object#number) have additional configuration inside the `number` property. In this example, the `format` configuration is set to `"dollar"` to control the appearance of page property values in this column.
### Iterate over a database object
A query to [Retrieve a database](/reference/retrieve-a-database) returns a database object. You can iterate over the `properties` object in the response to list information about each property. For example:
```javascript JavaScript theme={null}
Object.entries(database.properties).forEach(([propertyName, propertyValue]) => {
console.log(`${propertyName}: ${propertyValue.type}`);
});
```
## Adding pages to a data source
Pages are used as items inside a database, and each page's properties must conform to its parent database's schema. In other words, if you're viewing a database as a table, a page's properties define all the values in a single row.
**The page properties that are valid depend on the page's parent object.**
If you are [creating a page](/reference/post-page) in a database, the page properties must match the properties of the database. If you are creating a page that is not a child of a database, `title` is the only property that can be set.
Pages are added to a database using the [Create a page API endpoint](/reference/post-page). Let's try to add a page to the example database above.
The [Create a page](/reference/post-page) endpoint has two required parameters: `parent` and `properties`.
When adding a page to a database, the `parent` parameter must be a [database parent](/reference/parent-object). We can build this object for the example database above:
```js JSON theme={null}
{
"type": "data_source_id",
"data_source_id": "248104cd-477e-80af-bc30-000bd28de8f9"
}
```
**Permissions**
Before a connection can create a page within another page, it needs access to the page parent. To share a page with a connection, click the ••• menu at the top right of a page, scroll to `Add connections`, and use the search bar to find and select the connection from the dropdown list.
**Where can I find my database and data source's IDs?**
* Open the database as a full page in Notion.
* Use the `Share` menu to `Copy link`.
* Now paste the link in your text editor so you can take a closer look. The URL uses the following format:
```bash theme={null}
https://www.notion.com/{workspace_name}/{database_id}?v={view_id}
```
* Find the part that corresponds to `{database_id}` in the URL you pasted. It is a 36 character long string. This value is your **database ID**.
* Note that when you receive the database ID from the API, e.g. the [search](/reference/post-search) endpoint, it will contain hyphens in the UUIDv4 format. You may use either the hyphenated or un-hyphenated ID when calling the API.
* To get the **data source ID**, either use the [Retrieve a database](/reference/retrieve-database) endpoint first and check the `data_sources` array, or use the overflow menu under "Manage data sources" to copy it from the Notion app:
Continuing the create page example above, the `properties` parameter is an object that uses property names or IDs as keys, and [property value objects](/reference/page-property-values) as values. In order to create this parameter correctly, you refer to the [property objects](/reference/property-object) in the database's schema as a blueprint. We can build this object for the example database above too:
```json JSON theme={null}
{
"Grocery item": {
"type": "title",
"title": [{ "type": "text", "text": { "content": "Tomatoes" } }]
},
"Price": {
"type": "number",
"number": 1.49
},
"Last ordered": {
"type": "date",
"date": { "start": "2021-05-11" }
}
}
```
**Building a property value object in code**
Building the property value object manually, as described in this guide, is only helpful when you're working with one specific database that you know about ahead of time.
In order to build a connection that works with any database a user picks, and to remain flexible as the user's chosen database inevitably changes in the future, use the [Retrieve a database](/reference/retrieve-database) endpoint, followed by [Retrieve a data source](/reference/retrieve-a-data-source). Your connection can call this endpoint to get a current data source schema, and then create the `properties` parameter in code based on that schema.
Using both the `parent` and `properties` parameters, we create a page by sending a request to [the endpoint](/reference/post-page).
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/pages \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"parent": { "type": "data_source_id", "data_source_id": "248104cd-477e-80af-bc30-000bd28de8f9" },
"properties": {
"Grocery item": {
"type": "title",
"title": [{ "type": "text", "text": { "content": "Tomatoes" } }]
},
"Price": {
"type": "number",
"number": 1.49
},
"Last ordered": {
"type": "date",
"date": { "start": "2021-05-11" }
}
}
}'
```
```javascript JavaScript expandable theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const response = await notion.pages.create({
parent: {
data_source_id: '248104cd-477e-80af-bc30-000bd28de8f9',
},
properties: {
'Grocery item': {
type: 'title',
title: [
{
type: 'text',
text: {
content: 'Tomatoes',
},
},
],
},
Price: {
type: 'number',
number: 1.49,
},
'Last ordered': {
type: 'date',
date: {
start: '2021-05-11',
},
},
},
});
console.log(response);
})();
```
Once the page is added, you'll receive a response containing the new [page object](/reference/page). An important property in the response is the page ID (`id`). If you're connecting Notion to an external system, it's a good idea to store the page ID. If you want to update the page properties later, you can use the ID with the [Update page](/reference/patch-page) endpoint.
**Using a template**
When creating a page in the API, instead of populating the content manually, you can specify a data source template to apply.
Learn more about [database templates](https://www.notion.com/help/database-templates) in our Help Center, and then refer to the [Creating pages from templates](/guides/data-apis/creating-pages-from-templates) developer guide to get started.
## Finding pages in a data source
Pages can be read from a data source using the [Query a data source](/reference/query-a-data-source) endpoint. This endpoint allows you to find pages based on criteria such as "which page has the most recent *Last ordered date*". Some data sources are very large and this endpoint also allows you to get the results in a specific order, and get the results in smaller batches.
**Getting a specific page**
If you're looking for one specific page and already have its page ID, you don't need to query a data source to find it. Instead, use the [Retrieve a page](/reference/retrieve-a-page) endpoint.
### Filtering data source pages
The criteria used to find pages are called [filters](/reference/filter-data-source-entries). Filters can describe simple conditions (i.e. "*Tag* includes *Urgent*") or more complex conditions (i.e. "*Tag* includes *Urgent* AND *Due date* is within the next week AND *Assignee* equals *Cassandra Vasquez*"). These complex conditions are called [compound filters](/reference/filter-data-source-entries#compound-filter-conditions) because they use "and" or "or" to join multiple single property conditions together.
**Finding all pages in a data source**
To find all non-archived pages in a data source, send a request to the [query a data source](/reference/query-a-data-source) without a `filter` parameter. To query archived pages instead, set `is_archived` to `true`.
In this guide, let's focus on a single property condition using the example data source above. Looking at the data source schema, we know the *Last ordered* property uses the type `"date"`. This means we can build a filter for the *Last ordered* property using any [condition for the `"date"` type](/reference/filter-data-source-entries#date). The following filter object matches pages where the *Last ordered* date is in the past week:
```js JavaScript theme={null}
{
"property": "Last ordered",
"date": {
"past_week": {}
}
}
```
Using this filter, we can find all the pages in the example database that match the condition.
```bash cURL theme={null}
curl -X POST https://api.notion.com/v1/data_sources/248104cd477e80afbc30000bd28de8f9/query \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"filter": {
"property": "Last ordered",
"date": {
"past_week": {}
}
}
}'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const dataSourceId = '248104cd-477e-80af-bc30-000bd28de8f9';
const response = await notion.dataSources.query({
data_source_id: dataSourceId,
filter: {
property: 'Last ordered',
date: {
past_week: {},
},
}
});
console.log(response);
})();
```
You'll receive a response that contains a list of matching [page objects](/reference/page).
```js JavaScript theme={null}
{
"object": "list",
"results": [
{
"object": "page",
/* details omitted */
}
],
"has_more": false,
"next_cursor": null
}
```
This is a paginated response. Paginated responses are used throughout the Notion API when returning a potentially large list of objects. The maximum number of results in one paginated response is 100. The [pagination reference](/reference/intro#pagination) explains how to use the `start_cursor` and `page_size` parameters to get more than 100 results.
A single query also returns at most 10,000 results in total. To read every page in a data source that holds more than that, see [Query large data sources](/guides/data-apis/query-large-data-sources).
### Sorting data source pages
In this case, the individual pages we requested are in the `"results"` array. What if our connection (or its users) cared most about pages that were created recently? It would be helpful if the results were ordered so that the most recently created page was first, especially if the results didn't fit into one paginated response.
The `sort` parameter is used to order results by individual properties or by timestamps. This parameter can be assigned an array of sort object.
The time which a page was created is not a page property (properties that conform to the data source schema). Instead, it's a property that every page has, and it's one of two kinds of timestamps. It is called the `"created_time"` timestamp. Let's build a [sort object](/reference/sort-data-source-entries) that orders results so the most recently created page is first:
```json JSON theme={null}
{
"timestamp": "created_time",
"direction": "descending"
}
```
Finally, let's update the request we made earlier to order the page results using this sort object:
```bash cURL theme={null}
curl -X POST https://api.notion.com/v1/data_sources/248104cd477e80afbc30000bd28de8f9/query \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"filter": {
"property": "Last ordered",
"date": {
"past_week": {}
}
},
"sorts": [{ "timestamp": "created_time", "direction": "descending" }]
}'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const dataSourceId = '248104cd477e80afbc30000bd28de8f9';
const response = await notion.dataSources.query({
data_source_id: dataSourceId,
filter: {
property: 'Last ordered',
date: {
past_week: {},
},
},
sorts: [
{
timestamp: 'created_time',
direction: 'descending',
},
]
});
console.log(response);
})();
```
## Conclusion
Understanding data source schemas, made from a collection of properties, is key to working with Notion databases. This enables you to add, query for, and manage pages to a data source.
You're ready to help users take advantage of Notion's flexible and extensible data source interface to work with more kinds of data. There's more to learn and do with data sources in the resources below.
### Next steps
* This guide explains working with page properties. Take a look at [working with page content](/guides/data-apis/working-with-page-content).
* Explore the [database object](/reference/database) and [data source object](/reference/data-source) to see their other attributes available in the API.
* Learn about the other [page property value](/reference/page-property-values) types. In particular, try to do more with [rich text](/reference/rich-text).
* Learn more about [pagination](/reference/intro#pagination).
# Working with files and media
Source: https://developers.notion.com/guides/data-apis/working-with-files-and-media
Learn how to add and retrieve files and media on pages with the Notion API.
Files, images, and other media bring your Notion workspace to life — from company logos and product photos to contract PDFs and design assets. With the Notion API, you can programmatically upload, attach, and reuse these files wherever they’re needed.
In this guide, you’ll learn how to:
* Upload a new file using the **Direct Upload** method (single-part)
* Retrieve existing files already uploaded to your workspace
We’ll also walk through the different upload methods and supported file types, so you can choose the best path for your connection.
## Upload methods at a glance
The Notion API supports three ways to add files to your workspace:
| Upload method | Description | Best for |
| :----------------------------------------------------------------------- | :------------------------------------------------------------- | :---------------------------------------- |
| [**Direct Upload**](/guides/data-apis/uploading-small-files) | Upload a file (≤ 20MB) via a `multipart/form-data` request | The simplest method for most files |
| [**Direct Upload (multi-part)**](/guides/data-apis/sending-larger-files) | Upload large files (> 20MB) in chunks across multiple requests | Larger media assets and uploads over time |
| [**Indirect Import**](/guides/data-apis/importing-external-files) | Import a file from a publicly accessible URL | Migration workflows and hosted content |
## Supported block types
Uploaded files can be attached to:
* Media blocks: `file`, `image`, `pdf`, `audio`, `video`
* `embed` blocks: attach an uploaded file to render it inline (see [Embed](/reference/block#embed))
* Page properties: `files` properties in databases
* Page-level visuals: page `icon` and `cover`
**Need support for another block or content type**? Let us know [here](https://notiondevs.notion.site/1f8a4445d271805da593dd86bd86872b?pvs=105).
## Supported file types
Before uploading, make sure your file type is supported. Here’s what the API accepts:
| Category | Extensions | MIME types |
| :----------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Audio** | .aac, .adts, .mid, .midi, .mp3, .mpga, .m4a, .m4b, .mp4, .oga, .ogg, .opus, .wav, .wma, .weba, .flac | audio/aac, audio/midi, audio/mpeg, audio/mp4, audio/ogg, audio/wav, audio/x-ms-wma, audio/webm, audio/x-flac |
| **Document** | .pdf, .txt, .csv, .json, .doc, .dot, .docx, .dotx, .xls, .xlt, .xla, .xlsx, .xltx, .ppt, .pot, .pps, .ppa, .pptx, .potx, .rtf, .md, .markdown, .html, .htm, .epub, .xml, .css, .odt, .ods, .odp, .ics, .yaml, .yml, .tsv, .zip, .gz, .gzip, .tar, .7z, .bz2, .rar | application/pdf, text/plain, text/csv, application/csv, application/json, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.wordprocessingml.template, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.openxmlformats-officedocument.spreadsheetml.template, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.presentationml.template, application/rtf, text/markdown, text/html, application/epub+zip, text/xml, application/xml, text/css, application/vnd.oasis.opendocument.text, application/vnd.oasis.opendocument.spreadsheet, application/vnd.oasis.opendocument.presentation, text/calendar, text/yaml, text/tab-separated-values, application/zip, application/gzip, application/x-tar, application/x-7z-compressed, application/x-bzip2, application/vnd.rar |
| **Image** | .gif, .heic, .jpeg, .jpg, .png, .svg, .tif, .tiff, .webp, .ico, .bmp, .avif, .apng | image/gif, image/heic, image/jpeg, image/png, image/svg+xml, image/tiff, image/webp, image/vnd.microsoft.icon, image/bmp, image/avif, image/apng |
| **Video** | .amv, .asf, .wmv, .avi, .f4v, .flv, .gifv, .m4v, .mp4, .mkv, .webm, .mov, .qt, .mpeg, .ogv, .3gp, .3g2 | video/x-amv, video/x-ms-asf, video/x-msvideo, video/x-f4v, video/x-flv, video/mp4, application/mp4, video/webm, video/quicktime, video/mpeg, video/ogg, video/3gpp, video/3gpp2 |
**Ensure your file type matches the context**
For example:
* You can’t use a video in an image block
* Page icons can’t be PDFs
* Text files can’t be embedded in video blocks
### File size limits
* **Free** workspaces are limited to **5 MiB (binary megabytes) per file**
* **Paid** workspaces are limited to **5 GiB per file**.
* Files larger than 20 MiB must be split into parts and [uploaded using multi-part mode](/guides/data-apis/sending-larger-files) in the API.
These are the same [size limits that apply](https://www.notion.com/pricing) to uploads in the Notion app UI.
Use the [Retrieve a user](/reference/get-user) or [List all users](/reference/get-users) API to get the file size limit for a [bot user](/reference/user#bots). Public connections that can be added to both free or paid workspaces can retrieve or cache each bot's file size limit. This can help avoid HTTP 400 validation errors for attempting to [send](/reference/upload-file) files above the size limit.
```typescript Bot user API response shape theme={null}
type APIUserObject = {
object: "user",
type: "bot",
// ... other fields omitted
bot: {
// ... other fields omitted
// Limits and restrictions that apply to the bot's workspace.
workspace_limits: {
// The maximum allowable size of a file upload, in bytes.
max_file_upload_size_in_bytes: number,
},
}
}
```
For example, in a free workspace where bots are limited to FileUploads of 5 MiB, the response looks like:
```json Example user API object response theme={null}
{
"object": "user",
"id": "be51669b-1932-4a11-8d35-38fbc2e1e4fd",
"type": "bot",
"bot": {
"owner": {
"type": "workspace"
},
"workspace_name": "Cat's Notion",
"workspace_limits": {
"max_file_upload_size_in_bytes": 5242880
}
}
}
```
### Other limitations
The rest of the pages in this guide, as well as the API reference for the File Upload API, include additional validations and restrictions to keep in mind as you build your connection and send files.
One final limit to note here is both the [Create a file upload](/reference/create-file) and [Send a file upload](/reference/upload-file) APIs allow a maximum length of a `filename` (including the extension) of 900 bytes. However, we recommend using shorter names for performance and easier file management and lookup using the [List file uploads](/reference/list-file-uploads) API.
**What’s Next**
Now that you know what’s supported, let’s walk through a real upload using the simplest method: uploading a single file in one request.
# Working with markdown content
Source: https://developers.notion.com/guides/data-apis/working-with-markdown-content
Learn how to create, read, and update Notion page content using enhanced markdown instead of the block API.
## Overview
The Notion API supports reading, writing, and updating page content using **enhanced markdown** (also called "Notion-flavored Markdown") as an alternative to the [block-based API](/guides/data-apis/working-with-page-content). This is especially useful for agentic systems and developer tools that work natively with markdown.
Three API surfaces are available:
| Operation | Endpoint | Description |
| --------- | ----------------------------------- | --------------------------------------------------------------- |
| Create | `POST /v1/pages` | Create a page with markdown content (via `markdown` body param) |
| Read | `GET /v1/pages/:page_id/markdown` | Retrieve a page's full content as markdown |
| Update | `PATCH /v1/pages/:page_id/markdown` | Insert or replace content using markdown |
All three endpoints use the same **enhanced markdown** format. See the [Enhanced markdown format reference](/guides/data-apis/enhanced-markdown) for the full specification.
## Block type support
The markdown API supports most Notion block types. The table below shows how each block type maps to its markdown representation.
### Supported block types
| Block type | Markdown format |
| --------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [Paragraph](/reference/block#paragraph) | Plain text |
| [Heading 1 / 2 / 3 / 4](/reference/block#headings) | `#` / `##` / `###` / `####` |
| [Bulleted list item](/reference/block#bulleted-list-item) | `- item` |
| [Numbered list item](/reference/block#numbered-list-item) | `1. item` |
| [To do](/reference/block#to-do) | `- [ ]` / `- [x]` |
| [Toggle](/reference/block#toggle-blocks) | `` / `` |
| [Quote](/reference/block#quote) | `> quote` |
| [Callout](/reference/block#callout) | `` |
| [Divider](/reference/block#divider) | `---` |
| [Code](/reference/block#code) | Fenced code block with language |
| [Equation](/reference/block#equation) | `$$ equation $$` |
| [Table](/reference/block#table) | `
` with `
` and `
` |
| [Image](/reference/block#image) | `` |
| [File](/reference/block#file) | `caption` |
| [Video](/reference/block#video) | `` |
| [Audio](/reference/block#audio) | `` |
| [PDF](/reference/block#pdf) | `caption` |
| [Child page](/reference/block#child-page) | `title` |
| [Child database](/reference/block#child-database) | `title` |
| [Synced block](/reference/block#synced-block) | `` with content |
| [Column list / Column](/reference/block#column-list-and-column) | `` / `` |
| [Table of contents](/reference/block#table-of-contents) | `` |
| [Transcription](/reference/block#transcription) | `` (transcript included when `include_transcript=true`) |
For file-based blocks (image, file, video, audio, PDF), the URLs in the markdown output are pre-signed and ready to download. They expire after a short period, consistent with the [block-based API](/reference/block#file).
### Unsupported block types
The following block types are not yet rendered in the markdown output. When encountered, they appear as `` tags. The `url` links to the block in Notion, and `alt` indicates the original block type.
| Block type | Notes |
| --------------------------------------------- | ------------------------------- |
| [Bookmark](/reference/block#bookmark) | Web bookmarks with URL previews |
| [Embed](/reference/block#embed) | Embedded third-party content |
| [Link preview](/reference/block#link-preview) | Unfurled URL previews |
| [Breadcrumb](/reference/block#breadcrumb) | Navigation breadcrumbs |
| [Template](/reference/block#template) | Template buttons (deprecated) |
Block types that are not recognized by the block API (returned as `"unsupported"`) will also appear as `` in the markdown output.
You can use the [block-based API](/reference/block) to retrieve structured data for any unsupported block types you encounter in the markdown output.
## Creating a page with markdown
Use `POST /v1/pages` with the `markdown` parameter instead of `children` to create a page from a markdown string.
The `markdown` field expects actual newline characters. In JSON, use `\n` to encode them — for example, `"# Heading\nParagraph"`. To create a line break inside a single paragraph block, use ` `. When using cURL, wrap the `--data` body in **single quotes** so that `\n` is preserved for the JSON parser.
```bash cURL theme={null}
curl -X POST https://api.notion.com/v1/pages \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"parent": { "page_id": "YOUR_PAGE_ID" },
"markdown": "# Meeting Notes\nDiscussed roadmap priorities.\n## Action items\n- [ ] Draft proposal\n- [ ] Schedule follow-up"
}'
```
```javascript JavaScript theme={null}
const { Client } = require("@notionhq/client");
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const response = await notion.pages.create({
parent: { page_id: "YOUR_PAGE_ID" },
markdown: "# Meeting Notes\nDiscussed roadmap priorities.\n## Action items\n- [ ] Draft proposal\n- [ ] Schedule follow-up",
});
```
**Key behaviors:**
* The `markdown` parameter is mutually exclusive with `children` and `content`. You cannot use both.
* If `properties.title` is omitted, the first `# h1` heading is extracted as the page title.
* Available to all connection types (public, internal, and personal access tokens).
* Requires `insert_content` and `insert_property` capabilities.
The response is a standard [page object](/reference/page).
## Retrieving a page as markdown
Use `GET /v1/pages/:page_id/markdown` to retrieve a page's content rendered as enhanced markdown.
```bash cURL theme={null}
curl 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const response = await notion.pages.retrieveMarkdown({
page_id: "YOUR_PAGE_ID",
});
console.log(response.markdown);
```
**Response:**
```json theme={null}
{
"object": "page_markdown",
"id": "page-uuid",
"markdown": "# Meeting Notes\nDiscussed roadmap priorities.\n## Action items\n- [ ] Draft proposal\n- [ ] Schedule follow-up",
"truncated": false,
"unknown_block_ids": []
}
```
Retrieved markdown uses a single newline (`\n`) between adjacent top-level blocks. Line breaks inside a single block are represented as ` `.
### Query parameters
| Parameter | Type | Description |
| -------------------- | ------- | ---------------------------------------------------- |
| `include_transcript` | boolean | Include meeting note transcripts (default: `false`). |
```bash cURL (with transcript) theme={null}
curl 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown?include_transcript=true' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const response = await notion.pages.retrieveMarkdown({
page_id: "YOUR_PAGE_ID",
include_transcript: true,
});
```
**Key behaviors:**
* Available to all connection types (public, internal, and personal access tokens).
* Requires `read_content` capability.
* File URIs in the content are automatically converted to pre-signed URLs.
### Unknown blocks, truncation, and permissions
Some blocks in a page may appear as `` tags in the markdown output. This can happen for two reasons:
1. **Truncation** — the page exceeds the record limit (approximately 20,000 blocks) and some blocks were not loaded.
2. **Permissions** — the page contains child pages or other content that is not shared with the connection. The connection can access the parent page, but not those specific child blocks.
In both cases:
* The `truncated` field is set to `true`.
* The affected blocks appear as `` tags in the markdown.
* The `unknown_block_ids` array contains the IDs of these blocks.
```json theme={null}
{
"object": "page_markdown",
"id": "page-uuid",
"markdown": "# Large Document\nFirst section content...\n",
"truncated": true,
"unknown_block_ids": ["def456-with-dashes-uuid"]
}
```
You can attempt to fetch the content of unknown blocks by passing their IDs back to the same endpoint:
```bash cURL (fetching an unknown block) theme={null}
curl 'https://api.notion.com/v1/pages/UNKNOWN_BLOCK_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const blockResp = await notion.pages.retrieveMarkdown({
page_id: "UNKNOWN_BLOCK_ID",
});
```
For blocks that were unknown due to truncation, this returns the subtree rooted at that block. For blocks that are unknown due to permissions, the request returns an `object_not_found` error — the connection does not have access to that content.
The `unknown_block_ids` array does not distinguish between truncated and inaccessible blocks. When re-fetching unknown block IDs, handle `object_not_found` errors gracefully as they indicate blocks the connection cannot access.
For the best experience, keep pages under a few thousand blocks. Very large pages may require multiple requests to fully retrieve.
**Example: iteratively fetching a large page**
```python Python theme={null}
import requests
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2026-03-11",
}
resp = requests.get(
f"https://api.notion.com/v1/pages/{page_id}/markdown",
headers=headers,
).json()
all_markdown = resp["markdown"]
for block_id in resp.get("unknown_block_ids", []):
block_resp = requests.get(
f"https://api.notion.com/v1/pages/{block_id}/markdown",
headers=headers,
).json()
all_markdown += "\n" + block_resp["markdown"]
```
```javascript JavaScript theme={null}
const response = await notion.pages.retrieveMarkdown({
page_id: "YOUR_PAGE_ID",
});
let allMarkdown = response.markdown;
for (const blockId of response.unknown_block_ids) {
const blockResp = await notion.pages.retrieveMarkdown({
page_id: blockId,
});
allMarkdown += "\n" + blockResp.markdown;
}
```
## Updating a page with markdown
Use `PATCH /v1/pages/:page_id/markdown` to insert or replace content in an existing page using markdown.
The request body uses a **discriminated union** with four command variants. We recommend `update_content` and `replace_content` for new connections — they offer more precise control and better performance than the older `insert_content` and `replace_content_range` commands.
The `content` field expects enhanced markdown with actual newline characters. In your JSON request body, use `\n` to encode newlines — for example, `"## Heading\nParagraph text"` creates a heading followed by a paragraph block. To create a line break inside a single paragraph block, use ` `. Literal backslash-n sequences (like typing `\n` into a form field) will not be interpreted as newlines.
When using cURL, wrap the `--data` body in **single quotes** so that `\n` is preserved for the JSON parser. Avoid `$'...'` quoting, which converts `\n` into a literal newline and produces invalid JSON.
### Updating content with search-and-replace
Use `update_content` to make targeted edits with an array of search-and-replace operations. Each operation specifies `old_str` (content to find) and `new_str` (replacement content).
```bash cURL theme={null}
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"type": "update_content",
"update_content": {
"content_updates": [
{
"old_str": "Draft proposal",
"new_str": "Draft proposal (due Friday)"
},
{
"old_str": "Schedule follow-up",
"new_str": "Schedule follow-up with design team"
}
]
}
}'
```
```javascript JavaScript theme={null}
const response = await notion.pages.updateMarkdown({
page_id: "YOUR_PAGE_ID",
type: "update_content",
update_content: {
content_updates: [
{ old_str: "Draft proposal", new_str: "Draft proposal (due Friday)" },
{ old_str: "Schedule follow-up", new_str: "Schedule follow-up with design team" },
],
},
});
```
Each `old_str` must match exactly one location in the page. If it matches multiple locations, a `validation_error` is returned — set `replace_all_matches: true` on that operation to replace all occurrences.
### Replacing all page content
Use `replace_content` to replace the entire page content with new markdown.
```bash cURL theme={null}
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"type": "replace_content",
"replace_content": {
"new_str": "# Fresh Start\nThis replaces all previous content."
}
}'
```
```javascript JavaScript theme={null}
const response = await notion.pages.updateMarkdown({
page_id: "YOUR_PAGE_ID",
type: "replace_content",
replace_content: {
new_str: "# Fresh Start\nThis replaces all previous content.",
},
});
```
### Legacy commands
The `insert_content` and `replace_content_range` commands are still supported but are no longer recommended. They use an ellipsis-based selection format that is less precise than the search-and-replace approach of `update_content`. New connections should use `update_content` or `replace_content` instead.
Insert new markdown content at the start of a page, after a specific point in the page, or at the end.
```bash cURL (prepend to start) theme={null}
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"type": "insert_content",
"insert_content": {
"content": "## Latest update\nAdded at the top of the page.",
"position": { "type": "start" }
}
}'
```
```javascript JavaScript theme={null}
const response = await notion.pages.updateMarkdown({
page_id: "YOUR_PAGE_ID",
type: "insert_content",
insert_content: {
content: "## Latest update\nAdded at the top of the page.",
position: { type: "start" },
},
});
```
```bash cURL (insert after selection) theme={null}
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"type": "insert_content",
"insert_content": {
"content": "## New Section\nInserted content here.",
"after": "# Meeting Notes...Action items"
}
}'
```
```javascript JavaScript theme={null}
const response = await notion.pages.updateMarkdown({
page_id: "YOUR_PAGE_ID",
type: "insert_content",
insert_content: {
content: "## New Section\nInserted content here.",
after: "# Meeting Notes...Action items",
},
});
```
```bash cURL (append to end) theme={null}
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"type": "insert_content",
"insert_content": {
"content": "## Appendix\nAdded at the end of the page.",
"position": { "type": "end" }
}
}'
```
```javascript JavaScript theme={null}
const response = await notion.pages.updateMarkdown({
page_id: "YOUR_PAGE_ID",
type: "insert_content",
insert_content: {
content: "## Appendix\nAdded at the end of the page.",
position: { type: "end" },
},
});
```
The `position` parameter supports `{ "type": "start" }` and `{ "type": "end" }`. When both `position` and `after` are omitted, content is appended to the end of the page, preserving the existing behavior.
The `after` parameter uses an **ellipsis-based selection** format: `"start text...end text"`. This matches a range from the first occurrence of the start text to the end text. Do not provide `after` and `position` in the same request.
Replace a matched range of existing content with new markdown.
```bash cURL theme={null}
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"type": "replace_content_range",
"replace_content_range": {
"content": "## Updated Section\nNew content replaces the old.",
"content_range": "## Old Section...end of old content"
}
}'
```
```javascript JavaScript theme={null}
const response = await notion.pages.updateMarkdown({
page_id: "YOUR_PAGE_ID",
type: "replace_content_range",
replace_content_range: {
content: "## Updated Section\nNew content replaces the old.",
content_range: "## Old Section...end of old content",
},
});
```
The `content_range` parameter uses the same ellipsis-based selection as `after`.
### Safety: protecting child pages and databases
By default, the update endpoint refuses to delete child pages or databases. If an operation would delete them, a `validation_error` is returned listing the affected items.
To allow deletion, set `allow_deleting_content: true` in the command body. This option is supported by `replace_content_range`, `update_content`, and `replace_content`:
```json theme={null}
{
"type": "replace_content",
"replace_content": {
"new_str": "Replacement content.",
"allow_deleting_content": true
}
}
```
### Update response
All variants return the full page content as markdown after the update:
```json theme={null}
{
"object": "page_markdown",
"id": "page-uuid",
"markdown": "...full page content after update...",
"truncated": false,
"unknown_block_ids": []
}
```
**Key behaviors:**
* Available to all connection types (public, internal, and personal access tokens).
* Requires `update_content` capability.
* The `content_range` / `after` / `old_str` matching is case-sensitive.
### Error responses
| Error code | Condition |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `validation_error` | The `content_range` or `after` selection does not match any content in the page, or an `old_str` in `update_content` is not found. |
| `validation_error` | Both `insert_content.after` and `insert_content.position` are provided. Use only one insertion target. |
| `validation_error` | An `old_str` in `update_content` matches multiple locations and `replace_all_matches` is not `true`. |
| `validation_error` | The operation would delete child pages or databases and `allow_deleting_content` is not `true`. The error message lists the affected items. |
| `validation_error` | The provided ID is a database or non-page block (use the appropriate API for those record types). |
| `validation_error` | The target page is a synced page (`external_object_instance_page`). Synced pages cannot be updated. |
| `object_not_found` | The page does not exist or the connection does not have access to it. |
| `restricted_resource` | The connection lacks `update_content` capability. |
### Meeting note transcripts
The update endpoint always skips meeting note transcript content, matching the default behavior of the GET endpoint. If you retrieve a page with `include_transcript=true`, the transcript text will appear in the response but cannot be used in `content_range` or `after` selections — the update endpoint does not see transcript content during matching and will return a `validation_error` for selections that span transcript text.
## Running large markdown writes asynchronously
Create and update requests with large markdown bodies can take longer than typical HTTP client, browser, or edge timeout budgets. For those writes, set `allow_async: true` to receive an `async_task` handle and poll for completion.
Async support is available for:
| Surface | Operation |
| ------- | ------------------------------------------------------------------------ |
| REST | `POST /v1/pages` when the request includes the `markdown` body parameter |
| REST | `PATCH /v1/pages/:page_id/markdown` |
| MCP | `create_pages` |
| MCP | `update_page` |
Requests that omit `allow_async`, or set it to `false`, keep the existing synchronous response behavior. `allow_async` changes response behavior only; it does not change validation, permissions, or the operation being performed. Notion does not automatically convert a synchronous request into an async response.
Async task completion is polling-first in this version. Use `poll_after_seconds` from the task response as the minimum delay before polling again. Webhook notifications and ETA estimates are not part of the async task contract.
### REST: Create a page asynchronously
Set `allow_async: true` on `POST /v1/pages` when creating a page from markdown:
```bash theme={null}
curl -X POST https://api.notion.com/v1/pages \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"parent": { "page_id": "YOUR_PAGE_ID" },
"markdown": "# Migration plan\n\nLarge markdown content...",
"allow_async": true
}'
```
The initial response is HTTP `202`:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "queued",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"poll_after_seconds": 2,
"operation": {
"surface": "rest",
"name": "POST /v1/pages"
}
}
```
### REST: Update markdown asynchronously
Set `allow_async: true` at the top level of the `PATCH /v1/pages/:page_id/markdown` request body:
```bash theme={null}
curl -X PATCH 'https://api.notion.com/v1/pages/YOUR_PAGE_ID/markdown' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"allow_async": true,
"type": "replace_content",
"replace_content": {
"new_str": "# Updated plan\n\nLarge replacement markdown..."
}
}'
```
The initial response is an `async_task` object with a `status_url` to poll. A `202` response means Notion accepted the request for background execution after initial request and access checks. Markdown parsing, content matching, and other update validation can still fail while the task runs, so always poll until the task reaches `succeeded` or `failed`.
### Poll for completion
Poll the task's `status_url`, or call [Retrieve an async task](/reference/retrieve-async-task) with the returned `id`:
```bash theme={null}
curl 'https://api.notion.com/v1/async_tasks/task_abc123' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
In the [SDK for JavaScript and TypeScript](https://github.com/makenotion/notion-sdk-js) (v5.23.0 and later), `pages.create()` and `pages.updateMarkdown()` accept `allow_async: true`, and `asyncTasks.retrieve()` polls the returned task:
```javascript theme={null}
const task = await notion.asyncTasks.retrieve({ task_id: "task_abc123" })
```
An active task returns a non-terminal status and another polling hint:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "running",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"poll_after_seconds": 2,
"operation": {
"surface": "rest",
"name": "PATCH /v1/pages/:page_id/markdown"
}
}
```
A successful update task includes the same result shape as the synchronous update:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "succeeded",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"operation": {
"surface": "rest",
"name": "PATCH /v1/pages/:page_id/markdown"
},
"result": {
"object": "page_markdown",
"id": "page-uuid",
"markdown": "# Updated plan\n\nLarge replacement markdown...",
"truncated": false,
"unknown_block_ids": []
}
}
```
A failed task includes the standard Public API error shape:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "failed",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"operation": {
"surface": "rest",
"name": "PATCH /v1/pages/:page_id/markdown"
},
"error": {
"object": "error",
"status": 400,
"code": "validation_error",
"message": "The request body was invalid."
}
}
```
If a task is `retrying`, Notion is retrying a retryable infrastructure or downstream-service failure. Continue polling with the returned `poll_after_seconds` guidance. If a task reaches `failed`, inspect the error before retrying; validation and permission failures usually require a corrected request.
Completed and failed task metadata is retained for a bounded period. After expiry, polling the task returns the standard not-found response, so store any final result data your application needs.
For the full status resource, see [Retrieve an async task](/reference/retrieve-async-task).
### MCP async examples
For Notion MCP, pass `allow_async: true` to `create_pages` or `update_page`. The tools keep their normal synchronous behavior when `allow_async` is omitted.
```json theme={null}
{
"tool": "notion-create-pages",
"arguments": {
"allow_async": true,
"parent": { "page_id": "YOUR_PAGE_ID" },
"pages": [
{
"properties": { "title": "Migration plan" },
"content": "# Migration plan\n\nLarge markdown content..."
}
]
}
}
```
```json theme={null}
{
"tool": "notion-update-page",
"arguments": {
"allow_async": true,
"page_id": "YOUR_PAGE_ID",
"command": "replace_content",
"new_str": "# Updated plan\n\nLarge replacement markdown..."
}
}
```
Poll the task with `notion-get-async-task`:
```json theme={null}
{
"tool": "notion-get-async-task",
"arguments": {
"task_id": "task_abc123"
}
}
```
The MCP status values are the same as REST: `queued`, `running`, `retrying`, `succeeded`, and `failed`. When the task succeeds, the response includes the create or update result. When it fails, the response includes an error object.
## Access control summary
| Endpoint | Public connections | Internal connections | Personal access tokens | Required capability |
| ----------------------------- | ------------------ | -------------------- | ---------------------- | ------------------- |
| Create (`POST /v1/pages`) | Yes | Yes | Yes | `insert_content` |
| Read (`GET .../markdown`) | Yes | Yes | Yes | `read_content` |
| Update (`PATCH .../markdown`) | Yes | Yes | Yes | `update_content` |
# Working with page content
Source: https://developers.notion.com/guides/data-apis/working-with-page-content
Learn about page content and how to add or retrieve it with the Notion API.
## Overview
[Pages](https://www.notion.com/help/category/write-edit-and-customize) are where users write everything from quick notes, to shared documents, to curated landing pages in Notion. Connections can help users turn Notion into the single source of truth by syndicating content or help users gather, connect, and visualize content inside Notion.
In this guide, you'll learn about how the building blocks of page content are represented in the API and what you can do with them. By the end, you'll be able to create new pages with content, read content from other pages, and add blocks to existing pages.
### Page content versus properties
In general, **page properties** are best for capturing structured information such as a due date, a category, or a relationship to another page. **Page content** is best for looser structures or free form content. Page content is where users compose their thoughts or tell a story. Page properties are where users capture data and build systems. Your connection should aim to use each in the way users expect.
## Modeling content as blocks
A page's content is represented by a list of [block objects](/reference/block). These blocks are referred to as the page's children. Each block has a type, such as a paragraph, a heading, or an image. Some types of blocks, such as a toggle list, have children of their own.
Let's start with a simple example, a [paragraph block](/reference/block#paragraph):
```js JavaScript theme={null}
{
"object": "block",
"id": "380c78c0-e0f5-4565-bdbd-c4ccb079050d",
"type": "paragraph",
"created_time": "",
"last_edited_time": "",
"has_children": false,
"paragraph": {
"rich_text": [{
"type": "text",
"text": { "content": "Grocery List" }
}]
}
}
```
Paragraph blocks include common properties which every block includes: `object`, `type`, `created_time`, `last_edited_time`, and `has_children`. In addition, it contains type-specific information inside the `paragraph` property. Paragraph blocks have a `rich_text` property. Other block types have different type-specific properties.
Now let's look at an example where the block has child blocks: a paragraph followed by an indented [todo block](/reference/block#to-do):
```js JavaScript expandable theme={null}
{
"object": "block",
"id": "380c78c0-e0f5-4565-bdbd-c4ccb079050d",
"type": "paragraph",
"created_time": "",
"last_edited_time": "",
"has_children": true,
"paragraph": {
"rich_text": [{
"type": "text",
"text": { "content": "Grocery List" }
}],
"children": [
{
"object": "block",
"id": "6d5b2463-a1c1-4e22-9b3b-49b3fe7ad384",
"type": "to_do",
"created_time": "",
"last_edited_time": "",
"has_children": false,
"to_do": {
"rich_text": [{
"type": "text",
"text": { "content": "Buy kale" }
}],
"checked": false
}
}
]
}
}
```
Child blocks are represented as a list of blocks inside the type-specific property. When a block has children, the `has_children` property is `true`. Only some block types, like paragraph blocks, support children.
**Pages are also blocks**
Pages are a special kind of block, but they have children like many other block types. When [retrieving a list of child blocks](/reference/get-block-children), you can use the page ID as a block ID.
When a child page appears inside another page, it's represented as a `child_page` block, which does not have children. You should think of this as a reference to the page block.
**Unsupported block types**
The Notion API currently supports a subset of Notion [block](/reference/block#block-type-objects) types, with support for more coming soon. When an unsupported block type appears in a page, it will have the type `"unsupported"`.
### Rich text
In the previous block examples, the value of the `rich_text` property is a list of [rich text objects](/reference/rich-text). Rich text objects can describe more than a simple string - the object includes style information, links, mentions, and more.
Let's look at a simple example that just contains the words "Grocery List":
```js JavaScript theme={null}
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Grocery List",
"href": null
}
```
Rich text objects follow a similar pattern for type-specific configuration. The rich text object above has a type of `"text"`, and it has additional configuration related to that type in the `text` property. Other information that does not depend on the type, such as `annotations`, `plain_text`, and `href`, are at the top level of the rich text object.
Rich text is used both in page content and inside [page property values](/reference/page-property-values).
## Creating a page with content
Pages can be created with child blocks using the [create a page](/reference/post-page) endpoint. This endpoint supports creating a page within another page, or creating a page within a database.
Let's try creating a page within another page with some sample content. We will use all three parameters for this endpoint. The parent parameter is a [page parent](/reference/parent-object#page-parent). We can build this object using an existing page ID:
```js JavaScript theme={null}
{
"type": "page_id",
"page_id": "494c87d0-72c4-4cf6-960f-55f8427f7692"
}
```
**Permissions**
Before a connection can create a page within another page, it needs access to the page parent. To share a page with a connection, click the `•••` menu at the top right of a page, scroll to `Add connections`, and use the search bar to find and select the connection from the dropdown list.
**Where can I find my page's ID?**
Here's a quick procedure to find the page ID for a specific page in Notion:
Open the page in Notion.
Use the Share menu to Copy link.
Now paste the link in your text editor so you can take a closer look.
The URL ends in a page ID. It should be a 32 character long string. Format this value by inserting hyphens (-) in the following pattern:
1. 8-4-4-4-12 (each number is the length of characters between the hyphens).
2. Example: `1429989fe8ac4effbc8f57f56486db54` becomes `1429989f-e8ac-4eff-bc8f-57f56486db54`.
3. This value is your page ID.
While this procedure is helpful to try the API, **you shouldn't ask users to do this for your connection**. It's more common for a connection to determine a page ID by calling the [search API](/reference/post-search).
The `properties` parameter is an object which describes the page properties. Let's use a simple example with only the required `title` property:
```js JavaScript theme={null}
{
"Name": {
"type": "title",
"title": [{ "type": "text", "text": { "content": "A note from your pals at Notion" } }]
}
}
```
**Page properties within a database**
Pages within a database parent require properties to conform to the database's schema. Follow the [working with databases guide](/guides/data-apis/working-with-databases) for an in-depth discussion with examples.
The children parameter is a list of [block objects]() which describe the page content. Let's use some sample content:
```js JavaScript theme={null}
[
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "You made this page using the Notion API. Pretty cool, huh? We hope you enjoy building with us." } }]
}
}
]
```
**Size limits**
When creating new blocks, keep in mind that the Notion API has [size limits](/reference/request-limits#size-limits) for the content.
Using all three of the parameters, we create a page by sending a request to [the endpoint](/reference/post-page).
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/pages \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"parent": { "page_id": "494c87d0-72c4-4cf6-960f-55f8427f7692" },
"properties": {
"title": {
"title": [{ "type": "text", "text": { "content": "A note from your pals at Notion" } }]
}
},
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "You made this page using the Notion API. Pretty cool, huh? We hope you enjoy building with us." } }]
}
}
]
}'
```
```javascript JavaScript expandable theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const response = await notion.pages.create({
parent: {
page_id: '494c87d072c44cf6960f55f8427f7692',
},
properties: {
title: {
type: 'title',
title: [
{
type: 'text',
text: {
content: 'A note from your pals at Notion',
},
},
],
},
},
children: [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: 'You made this page using the Notion API. Pretty cool, huh? We hope you enjoy building with us.',
},
},
],
},
},
],
});
console.log(response);
})();
```
Once the page is added, you'll receive a response containing the new [page object](/reference/page). Take a look inside Notion and view your new page.
## Reading blocks from a page
Page content can be read from a page using the [retrieve block children](/reference/get-block-children) endpoint. This endpoint returns a list of children for any block which supports children. While pages are a common starting point for reading block children, you can retrieve the block children of other kinds of blocks, too.
The `block_id` parameter is the ID of any existing block. If you're following from the example above, the response contained a page ID. Let's use that page ID to read the sample content from the page. We'll use `"16d8004e-5f6a-42a6-9811-51c22ddada12"` as the block ID.
Using this `block_id`, we retrieve the block children by sending a request to [the endpoint](/reference/get-block-children).
```curl cURL theme={null}
curl https://api.notion.com/v1/blocks/16d8004e-5f6a-42a6-9811-51c22ddada12/children?page_size=100 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const blockId = '16d8004e5f6a42a6981151c22ddada12';
const response = await notion.blocks.children.list({
block_id: blockId,
});
console.log(response);
})();
```
You'll receive a response that contains a list of block objects.
```js JavaScript theme={null}
{
"object": "list",
"results": [
{
"object": "block",
/* details omitted */
}
],
"has_more": false,
"next_cursor": null
}
```
This is a paginated response. Paginated responses are used throughout the Notion API when returning a potentially large list of objects. The maximum number of results in one paginated response is 100. The [pagination reference](/reference/pagination) explains how to use the "start\_cursor" and "page\_size" parameters to get more than 100 results.
In this case, the individual child blocks we requested are in the "results" array.
### Reading nested blocks
What happens when the results contain a block that has its own children? In this case, the response will not contain those children, but the `has_children` property will be `true`. If your connection needs a complete representation of a page's (or any block's) content, it should search the results for blocks with `has_children` set to `true`, and recursively call the [retrieve block children](/reference/get-block-children) endpoint.
Reading large pages may take some time. We recommend using asynchronous operations in your architecture, such as a job queue. You will also need to be mindful of [rate limits](/reference/request-limits#rate-limits) to appropriately slow down making new requests after the limit is met.
## Appending blocks to a page
Connections can add more content to a page by using the [append block children](/reference/patch-block-children) endpoint. Let's try to add another block to the page we created in the example above. This endpoint requires two parameters: `block_id` and `children`.
The `block_id` parameter is the ID of any existing block. If you're following from the example above, let's use the same page ID as the block ID: `"16d8004e-5f6a-42a6-9811-51c22ddada12"`.
The `children` parameter is a list of [block objects](/reference/block) which describe the content we want to append. Let's use some more sample content:
```js JavaScript theme={null}
[
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "– Notion API Team", "link": { "type": "url", "url": "https://twitter.com/NotionAPI" } } }]
}
}
]
```
Using both parameters, we append blocks by sending a request to [the endpoint](/reference/patch-block-children).
```bash cURL theme={null}
curl -X PATCH https://api.notion.com/v1/blocks/16d8004e-5f6a-42a6-9811-51c22ddada12/children \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "– Notion API Team", "link": { "type": "url", "url": "https://twitter.com/NotionAPI" } } }]
}
}
]
}'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
(async () => {
const blockId = '16d8004e5f6a42a6981151c22ddada12';
const response = await notion.blocks.children.append({
block_id: blockId,
children: [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: '– Notion API Team',
link: {
type: 'url',
url: 'https://twitter.com/NotionAPI',
},
},
},
],
},
},
],
});
console.log(response);
})();
```
You'll receive a response that contains the updated block. The response does not contain the child blocks, but it will show `has_children` set to `true`.
By default, new block children are appended at the end of the parent block. To place the block after a specific child block and not at the end, use the `position` body parameter. The `position` object supports three placement types: `after_block` (insert after a specific block), `start` (insert at the beginning), and `end` (the default when `position` is omitted). For example, if the parent `block_id` is for a block that contains a bulleted list, you can use `position` with `after_block` to insert the new block children after a specific list item.
```bash cURL theme={null}
curl -X PATCH https://api.notion.com/v1/blocks/16d8004e-5f6a-42a6-9811-51c22ddada12/children \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "– Notion API Team", "link": { "type": "url", "url": "https://twitter.com/NotionAPI" } } }]
}
}
],
"position": {
"type": "after_block",
"after_block": { "id": "" }
}
}'
```
## Working with AI meeting notes
[AI meeting notes](https://www.notion.com/help/ai-meeting-notes) in Notion automatically generate a summary, action-item notes, and a full transcript for recorded meetings. The API exposes this content through the [`meeting_notes` block type](/reference/block#meeting-notes).
A meeting notes block is a metadata container that lives on the page as a child block. It includes the meeting title, lifecycle status, calendar event details, recording timestamps, and — most importantly — pointers to three child blocks that hold the actual generated content:
* **Summary** (`summary_block_id`) — an AI-generated overview of the meeting.
* **Notes** (`notes_block_id`) — structured action items and key points.
* **Transcript** (`transcript_block_id`) — the full word-for-word transcript.
### Retrieving AI meeting notes content
Meeting notes blocks cannot be created or updated with the general block endpoints. Use [Create a meeting note](/reference/create-meeting-note) to create one from uploaded media. To access generated content, fetch the page's children to find the meeting notes block, then follow the child block IDs to retrieve each section.
**Step 1 — List the page's children** to find the meeting notes block:
```bash cURL theme={null}
curl 'https://api.notion.com/v1/blocks/PAGE_ID/children' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const { Client, isFullBlock } = require("@notionhq/client");
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const { results } = await notion.blocks.children.list({
block_id: "PAGE_ID",
});
const meetingNotesBlock = results.find(
(block) => isFullBlock(block) && block.type === "meeting_notes"
);
```
**Step 2 — Read the child block IDs** from the meeting notes block's `children` field:
```json Example meeting_notes block response theme={null}
{
"object": "block",
"type": "meeting_notes",
"meeting_notes": {
"title": [{ "plain_text": "Team Sync" }],
"status": "notes_ready",
"children": {
"summary_block_id": "a1b2c3d4-...",
"notes_block_id": "b2c3d4e5-...",
"transcript_block_id": "c3d4e5f6-..."
},
"calendar_event": {
"start_time": "2026-02-24T10:00:00.000Z",
"end_time": "2026-02-24T10:45:00.000Z"
},
"recording": {
"start_time": "2026-02-24T10:00:00.000Z",
"end_time": "2026-02-24T10:45:00.000Z"
}
}
}
```
**Step 3 — Fetch each section's content** using the child block IDs. Each child is a standard block whose own children are the content (paragraphs, lists, etc.):
```bash cURL theme={null}
# Fetch the summary content
curl 'https://api.notion.com/v1/blocks/SUMMARY_BLOCK_ID/children' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const { children } = meetingNotesBlock.meeting_notes;
// Fetch the summary content blocks
const summary = await notion.blocks.children.list({
block_id: children.summary_block_id,
});
// Fetch the notes content blocks
const notes = await notion.blocks.children.list({
block_id: children.notes_block_id,
});
// Fetch the transcript content blocks
const transcript = await notion.blocks.children.list({
block_id: children.transcript_block_id,
});
```
The `status` field indicates whether the meeting notes are fully processed. Wait for `"notes_ready"` before fetching content — earlier statuses mean the AI is still generating output and the child blocks may not yet be available.
You can also retrieve meeting note content as markdown using the [Retrieve page markdown](/reference/retrieve-page-markdown) endpoint with the `include_transcript` query parameter. See [Working with markdown content](/guides/data-apis/working-with-markdown-content) for details.
## Conclusion
Nearly everything users see inside Notion is represented as blocks. Now that you've understood how your connection can build pages with blocks, read blocks, and add blocks to pages - you've unlocked most of the surface area in Notion. You connection can engage users where they do everything from creative writing, to building documentation, and more.
### Next steps
* This guide explains working with page content. Take a look at [working with database properties](/guides/data-apis/working-with-databases#database-properties).
* Explore the [block object](/reference/block) to see other types of blocks you can create.
* Learn more about the various kinds of [rich text objects](/reference/rich-text).
* Learn more about [pagination](/reference/intro#pagination).
# Working with views
Source: https://developers.notion.com/guides/data-apis/working-with-views
Learn how to set up and manage database views using the Notion API.
The Views API requires API version `2025-09-03` or later. If your connection uses an older version, see the [upgrade guide](/guides/get-started/upgrade-guide-2025-09-03) for migration steps.
## Overview
[Database views](https://www.notion.com/help/views-filters-and-sorts) let users see the same data in different ways — for example, as a table, board, calendar, timeline, gallery, list, form, chart, map, or dashboard. Each view can have its own filters, sorts, and layout configuration, so a single database can serve many different workflows.
The Notion API exposes views as first-class resources. This means connections can programmatically manage the same view presets that users create in the UI, enabling use cases like workspace bootstrapping, migration tooling, and automated view setup.
In this guide, you'll learn:
## Structure
A **view** is scoped to a single [data source](/reference/data-source) within a [database](/reference/database). It defines how pages in that data source are filtered, sorted, and displayed.
The view object looks like this:
```json View object example expandable theme={null}
{
"object": "view",
"id": "a3f1b2c4-5678-4def-abcd-1234567890ab",
"parent": {
"type": "database_id",
"database_id": "248104cd-477e-80fd-b757-e945d38000bd"
},
"data_source_id": "248104cd-477e-80af-bc30-000bd28de8f9",
"name": "High priority items",
"type": "table",
"filter": {
"property": "Priority",
"select": {
"equals": "High"
}
},
"sorts": [
{
"property": "Last ordered",
"direction": "descending"
}
],
"quick_filters": {
"Status": {
"status": { "equals": "In progress" }
}
},
"configuration": {
"type": "table",
"properties": [
{ "property_id": "title", "visible": true, "width": 300 },
{ "property_id": "abc1", "visible": true, "width": 200 },
{ "property_id": "def2", "visible": false }
],
"group_by": {
"type": "status",
"property_id": "ghi3",
"group_by": "group",
"sort": { "type": "manual" }
},
"wrap_cells": false,
"frozen_column_index": 1,
"show_vertical_lines": true
},
"created_time": "2026-01-15T10:30:00.000Z",
"last_edited_time": "2026-01-20T14:22:00.000Z",
"created_by": {
"object": "user",
"id": "e7f3a4b2-1234-5678-9abc-def012345678"
},
"last_edited_by": {
"object": "user",
"id": "e7f3a4b2-1234-5678-9abc-def012345678"
},
"url": "https://www.notion.com/example/248104cd477e80fdb757e945d38000bd?v=a3f1b2c45678"
}
```
Key fields:
* **`type`** — The layout type. One of: `table`, `board`, `list`, `calendar`, `timeline`, `gallery`, `form`, `chart`, `map`, or `dashboard`.
* **`data_source_id`** — Which data source this view is "over". A database can have multiple data sources, and each view targets exactly one. For dashboard views this is `null` since dashboards contain multiple widget views, each with their own data source.
* **`filter`** and **`sorts`** — Use the same shapes as the [filter](/reference/filter-data-source-entries) and [sort](/reference/sort-data-source-entries) parameters in data source queries.
* **`quick_filters`** — A map of property-level filters that appear in the view's filter bar. Keys are property names or IDs, values are filter conditions (same shape as property filters, without the `property` field). See [Quick filters](#quick-filters).
* **`configuration`** — Type-specific presentation settings that vary by view type. This is a discriminated union keyed on `type` — see [View configuration](#view-configuration) for the full schema per view type. This field is `null` when no custom configuration has been set.
* **`parent`** — Always a database. Views are retrieved and managed through their parent database.
* **`dashboard_view_id`** — Only present on widget views that belong to a dashboard. References the parent dashboard view's ID.
## Default behavior
When you [create a database](/reference/create-database) through the API, Notion automatically provisions:
1. One **data source** under the database container
2. One **Table view** named "Default view" over that data source
This means every newly created database is immediately usable — it has a data source to hold pages and a view to display them.
```bash cURL theme={null}
curl -X POST https://api.notion.com/v1/databases \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"parent": { "type": "page_id", "page_id": "YOUR_PAGE_ID" },
"title": [{ "type": "text", "text": { "content": "My Database" } }],
"is_inline": false
}'
```
```javascript JavaScript theme={null}
const { Client } = require("@notionhq/client");
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const database = await notion.databases.create({
parent: { type: "page_id", page_id: "YOUR_PAGE_ID" },
title: [{ type: "text", text: { content: "My Database" } }],
is_inline: false,
});
// database.data_sources[0].id is the auto-created data source
```
After creating the database, you can [list the views](#listing-views) to discover the default view, then create additional views as needed.
## Listing views
Use the [list endpoint](/reference/list-views) to discover views. You can filter by the view's parent `database_id` or by the `data_source_id` that the view references.
### By database
Pass `database_id` to list the views belonging to a specific database block.
```bash cURL theme={null}
curl -X GET "https://api.notion.com/v1/views?database_id=DATABASE_ID" \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const response = await notion.views.list({
database_id: "DATABASE_ID",
});
for (const view of response.results) {
console.log(view.id);
}
```
### By data source
Pass `data_source_id` to list all views that reference a given data source (collection), including linked views on other pages across the workspace.
```bash cURL theme={null}
curl -X GET "https://api.notion.com/v1/views?data_source_id=DATA_SOURCE_ID" \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const response = await notion.views.list({
data_source_id: "DATA_SOURCE_ID",
});
for (const view of response.results) {
console.log(view.id);
}
```
Results are filtered by your connection's access permissions. Views on pages the connection cannot access are excluded.
Both variants return a paginated list of view references:
```json theme={null}
{
"object": "list",
"results": [
{ "object": "view", "id": "a3f1b2c4-5678-4def-abcd-1234567890ab" },
{ "object": "view", "id": "b4e2c3d5-6789-5ef0-bcde-2345678901bc" }
],
"next_cursor": null,
"has_more": false
}
```
The list endpoint returns minimal view references (just `object` and `id`). To get full view details including filters, sorts, and configuration, retrieve each view individually.
## Retrieving a view
[Retrieve a view](/reference/retrieve-a-view) by its ID to see its full configuration, including filters, sorts, and layout settings.
```bash cURL theme={null}
curl -X GET https://api.notion.com/v1/views/VIEW_ID \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const view = await notion.views.retrieve({
view_id: "VIEW_ID",
});
console.log(view.name); // "High priority items"
console.log(view.type); // "table"
console.log(view.configuration); // { type: "table", properties: [...], ... }
```
The response is a full [view object](#structure).
## Creating a view
Create a new view by specifying the target data source, a name, and a view type. You must also provide one of `database_id` (to create a top-level view on a database), `view_id` (to add a widget view to an existing dashboard), or `create_database` (to create a linked database view on a page). You can optionally include filters, sorts, and a [configuration](#view-configuration) object. For the full parameter reference, see [Create a view](/reference/create-view).
`database_id` and `data_source_id` are different IDs. The `database_id` is the database container's ID (the same ID returned by the [Retrieve a database](/reference/retrieve-a-database) endpoint). The `data_source_id` is the ID of a specific data source within that database (found in the database's `data_sources` array). Most databases have a single data source, but both IDs are required.
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"database_id": "DATABASE_ID",
"data_source_id": "DATA_SOURCE_ID",
"name": "Recent orders",
"type": "table",
"filter": {
"property": "Last ordered",
"date": {
"past_week": {}
}
},
"sorts": [
{
"property": "Last ordered",
"direction": "descending"
}
],
"configuration": {
"type": "table",
"properties": [
{ "property_id": "title", "visible": true, "width": 300 },
{ "property_id": "abc1", "visible": true, "width": 200 }
],
"wrap_cells": true
}
}'
```
```javascript JavaScript expandable theme={null}
const view = await notion.views.create({
database_id: "DATABASE_ID",
data_source_id: "DATA_SOURCE_ID",
name: "Recent orders",
type: "table",
filter: {
property: "Last ordered",
date: {
past_week: {},
},
},
sorts: [
{
property: "Last ordered",
direction: "descending",
},
],
configuration: {
type: "table",
properties: [
{ property_id: "title", visible: true, width: 300 },
{ property_id: "abc1", visible: true, width: 200 },
],
wrap_cells: true,
},
});
console.log(view.id); // The new view's ID
console.log(view.url); // Deep link to the view in Notion
```
The response is the newly created [view object](#structure) with all fields populated.
Views in a database can be configured to show pages from a data source that's owned by another database. In Notion, this is called a [linked database](https://www.notion.com/help/data-sources-and-linked-databases) (or linked view), and it's useful for showing the same underlying data in multiple places—for example, putting filtered views of your Tasks, Projects, and Bugs on a single dashboard page.
In the API, the main requirement is that your connection has access to both the database that owns the data source, and the database you're creating the view in.
**Filtering by multiple select or status values**
Select, status, and multi\_select filter operators accept an array of strings to filter by multiple values at once. For example, to match Priority "Low" or "Medium":
```json theme={null}
{
"filter": {
"property": "Priority",
"select": { "equals": ["Low", "Medium"] }
}
}
```
You can also exclude multiple values:
```json theme={null}
{
"filter": {
"property": "Status",
"status": { "does_not_equal": ["Done", "Archive"] }
}
}
```
The API validates each value in the array against the property's configured options. For status properties, group names (e.g. "To-do", "In progress", "Complete") are also accepted. If any value doesn't match an existing option or group, the API returns a descriptive error listing the available values.
### Required parameters
| Parameter | Description |
| ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `data_source_id` | The ID of the data source this view is over. Retrieve this from the database object's `data_sources` array. |
| `name` | A display name for the view. |
| `type` | The view layout: `table`, `board`, `list`, `calendar`, `timeline`, `gallery`, `form`, `chart`, `map`, or `dashboard`. |
### Optional parameters
| Parameter | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `database_id` | The ID of the database to create the view in. Mutually exclusive with `view_id` and `create_database`. |
| `view_id` | The ID of a dashboard view to add this view to as a widget. Mutually exclusive with `database_id` and `create_database`. |
| `create_database` | Creates a linked database view on a page referencing an existing data source. See [Creating a linked database view](#creating-a-linked-database-view). Mutually exclusive with `database_id` and `view_id`. |
| `filter` | A [filter object](/reference/filter-data-source-entries) to apply. Uses the same shape as data source queries. |
| `sorts` | An array of [sort objects](/reference/sort-data-source-entries). Uses the same shape as data source queries. |
| `quick_filters` | A map of [quick filters](#quick-filters) for the view's filter bar. Keys are property names or IDs, values are filter conditions. |
| `configuration` | A [view configuration](#view-configuration) object. The `type` field inside must match the view `type`. |
| `position` | Where to place the new view in the database's view tab bar. Only applicable when `database_id` is provided. See [View positioning](#view-positioning). Defaults to appending at the end. |
| `placement` | Where to place the new widget in a dashboard layout. Only applicable when `view_id` is provided. See [Widget placement](#widget-placement). Defaults to creating a new row at the end. |
You must provide exactly one of `database_id`, `view_id`, or `create_database`. Use `database_id` to create a top-level view on a database. Use `view_id` to add a widget view to an existing dashboard — see [Dashboard views](#dashboard-views) for details. Use `create_database` to create a linked database view on a page — see [Creating a linked database view](#creating-a-linked-database-view).
**Finding the data source ID**
If you already have a database ID, call the [Retrieve a database](/reference/retrieve-database) endpoint. The response includes a `data_sources` array with each data source's `id` and `name`.
### Creating different view types
Here's an example of creating a Board view with grouping, cover images, and property configuration:
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"database_id": "DATABASE_ID",
"data_source_id": "DATA_SOURCE_ID",
"name": "Task board",
"type": "board",
"configuration": {
"type": "board",
"group_by": {
"type": "status",
"property_id": "STATUS_PROPERTY_ID",
"group_by": "group",
"sort": { "type": "manual" }
},
"cover": {
"type": "page_cover"
},
"cover_size": "medium",
"card_layout": "compact"
}
}'
```
```javascript JavaScript expandable theme={null}
const boardView = await notion.views.create({
database_id: "DATABASE_ID",
data_source_id: "DATA_SOURCE_ID",
name: "Task board",
type: "board",
configuration: {
type: "board",
group_by: {
type: "status",
property_id: "STATUS_PROPERTY_ID",
group_by: "group",
sort: { type: "manual" },
},
cover: {
type: "page_cover",
},
cover_size: "medium",
card_layout: "compact",
},
});
```
### View positioning
When creating a top-level database view (using `database_id`), you can control where it appears in the view tab bar with the `position` parameter. This is a discriminated union on the `type` field:
| Variant | Fields | Description |
| ------------ | ----------------- | --------------------------------------------------------- |
| `start` | `type` | Places the new view as the first tab. |
| `end` | `type` | Places the new view as the last tab (default). |
| `after_view` | `type`, `view_id` | Places the new view immediately after the specified view. |
```json Place at start theme={null}
{
"position": { "type": "start" }
}
```
```json Place after a specific view theme={null}
{
"position": {
"type": "after_view",
"view_id": "EXISTING_VIEW_ID"
}
}
```
The `position` parameter is only valid when `database_id` is provided. It cannot be used with `view_id` (dashboard widget creation).
### Creating a linked database view
Use the `create_database` parameter to create a lightweight linked database view on a page that references an existing data source. This creates a new database container on the target page with a single view over the specified data source — similar to inserting a "linked view of database" in the Notion UI.
This differs from `POST /v1/databases`, which creates a full standalone database with its own schema, data source, and default view. With `create_database`, the view points to an existing data source owned by another database, so no new schema is created.
```javascript JavaScript expandable theme={null}
const view = await notion.views.create({
create_database: {
parent: {
type: "page_id",
page_id: "TARGET_PAGE_ID",
},
},
data_source_id: "EXISTING_DATA_SOURCE_ID",
name: "Tasks overview",
type: "table",
});
```
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"create_database": {
"parent": {
"type": "page_id",
"page_id": "TARGET_PAGE_ID"
}
},
"data_source_id": "EXISTING_DATA_SOURCE_ID",
"name": "Tasks overview",
"type": "table"
}'
```
The `create_database` object accepts the following fields:
| Field | Required | Description |
| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parent` | Yes | The parent page for the linked database. Must be `{ "type": "page_id", "page_id": "..." }`. |
| `position` | No | Controls where the new database block appears within the parent page. Use `{ "type": "after_block", "block_id": "..." }` to place it after a specific block. The referenced block must be a direct child of the parent page. Defaults to appending at the end. |
All view types are supported with `create_database`, including `form` views with full form configuration and `dashboard` views. Dashboard views are created with an empty layout — add widgets to them via separate `POST /v1/views` calls with `view_id`.
Your connection must have access to both the target page (where the new database container is created) and the database that owns the data source being referenced.
## Updating a view
[Update a view](/reference/update-a-view) to change its name, filters, sorts, or configuration. All fields are optional — only include the fields you want to change.
```bash cURL expandable theme={null}
curl -X PATCH https://api.notion.com/v1/views/VIEW_ID \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"name": "Completed this month",
"filter": {
"and": [
{
"property": "Status",
"status": { "equals": "Done" }
},
{
"property": "Completed date",
"date": { "this_month": {} }
}
]
},
"sorts": [
{
"property": "Completed date",
"direction": "descending"
}
],
"configuration": {
"type": "table",
"group_by": null,
"properties": [
{ "property_id": "title", "visible": true, "width": 400 },
{ "property_id": "abc1", "visible": true },
{ "property_id": "def2", "visible": false }
]
}
}'
```
```javascript JavaScript expandable theme={null}
const updated = await notion.views.update({
view_id: "VIEW_ID",
name: "Completed this month",
filter: {
and: [
{
property: "Status",
status: { equals: "Done" },
},
{
property: "Completed date",
date: { this_month: {} },
},
],
},
sorts: [
{
property: "Completed date",
direction: "descending",
},
],
configuration: {
type: "table",
group_by: null,
properties: [
{ property_id: "title", visible: true, width: 400 },
{ property_id: "abc1", visible: true },
{ property_id: "def2", visible: false },
],
},
});
```
To clear a view's filter, sorts, or specific configuration fields, set them to `null`. See [Clearing configuration with null](#clearing-configuration-with-null) for concrete examples.
## Deleting a view
[Delete a view](/reference/delete-view) by its ID. This permanently removes the view from the database's view list.
```bash cURL theme={null}
curl -X DELETE https://api.notion.com/v1/views/VIEW_ID \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const deleted = await notion.views.delete({
view_id: "VIEW_ID",
});
// deleted.object === "view"
// deleted.id === "VIEW_ID"
```
The response is a partial view object containing only identity fields (`object`, `id`, `parent`, and `type`). Full view details like filters, sorts, and configuration are not included since the view has been deleted.
Deleting a view cannot be undone through the API. The view will no longer appear in the database's view list.
A database must always have at least one view. Attempting to delete the last remaining view returns a `validation_error`. To remove the database entirely, set [`in_trash`](/reference/update-database#body-in-trash) to `true` via the update database endpoint instead.
## View configuration
The `configuration` field on a view object controls type-specific presentation settings — things like column widths, grouping, cover images, subtasks, and more. It is a discriminated union keyed on the `type` field, which must match the view's top-level `type`.
You can pass `configuration` when [creating](#creating-a-view) or [updating](#updating-a-view) a view. Nullable fields accept `null` to clear the setting.
### Feature support by view type
| Feature | Table | Board | Calendar | Timeline | Gallery | List | Map | Form | Chart | Dashboard |
| ------------------------------------ | -------- | ------------ | ------------ | ------------ | -------- | ---- | -------- | -------- | ------------ | --------------- |
| `properties` | Yes | Yes | Yes | Yes | Yes | Yes | Optional | - | - | - |
| `group_by` | Optional | **Required** | - | - | - | - | - | - | - | - |
| `sub_group_by` | - | Optional | - | - | - | - | - | - | - | - |
| `subtasks` | Optional | - | - | - | - | - | - | - | - | - |
| `cover` | - | Optional | - | - | Optional | - | - | - | - | - |
| `cover_size` / `cover_aspect` | - | Optional | - | - | Optional | - | - | - | - | - |
| `card_layout` | - | Optional | - | - | Optional | - | - | - | - | - |
| `date_property_id` | - | - | **Required** | **Required** | - | - | - | - | - | - |
| `end_date_property_id` | - | - | - | Optional | - | - | - | - | - | - |
| `view_range` / `show_weekends` | - | - | Optional | - | - | - | - | - | - | - |
| `preference` / `arrows_by` | - | - | - | Optional | - | - | - | - | - | - |
| `show_table` / `table_properties` | - | - | - | Optional | - | - | - | - | - | - |
| `wrap_cells` / `frozen_column_index` | Optional | - | - | - | - | - | - | - | - | - |
| `show_vertical_lines` | Optional | - | - | - | - | - | - | - | - | - |
| `height` | - | - | - | - | - | - | Optional | - | Optional | - |
| `map_by` | - | - | - | - | - | - | Optional | - | - | - |
| `is_form_closed` | - | - | - | - | - | - | - | Optional | - | - |
| `anonymous_submissions` | - | - | - | - | - | - | - | Optional | - | - |
| `submission_permissions` | - | - | - | - | - | - | - | Optional | - | - |
| `chart_type` | - | - | - | - | - | - | - | - | **Required** | - |
| `x_axis` / `y_axis` | - | - | - | - | - | - | - | - | Optional | - |
| `value` | - | - | - | - | - | - | - | - | Optional | - |
| `rows` | - | - | - | - | - | - | - | - | - | Yes (read-only) |
### Table configuration
```json theme={null}
{
"type": "table",
"properties": [...],
"group_by": { ... } | null,
"subtasks": { ... } | null,
"wrap_cells": true,
"frozen_column_index": 1,
"show_vertical_lines": true
}
```
| Field | Type | Description |
| --------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `"table"` | **Required.** Must be `"table"`. |
| `properties` | array \| null | Property visibility and display settings. See [Property configuration](#property-configuration). |
| `group_by` | object \| null | Group rows by a property. See [Group-by configuration](#group-by-configuration). Pass `null` to remove. |
| `subtasks` | object \| null | Sub-item display settings. See [Subtask configuration](#subtask-configuration). Pass `null` to reset to defaults. Use `{ "display_mode": "disabled" }` to explicitly disable. |
| `wrap_cells` | boolean | Whether to wrap cell content. |
| `frozen_column_index` | integer (>= 0) | Number of columns frozen from the left. |
| `show_vertical_lines` | boolean | Whether to show vertical grid lines between columns. |
### Board configuration
```json theme={null}
{
"type": "board",
"group_by": { ... },
"sub_group_by": { ... } | null,
"properties": [...],
"cover": { "type": "page_cover" },
"cover_size": "medium",
"cover_aspect": "cover",
"card_layout": "compact"
}
```
| Field | Type | Description |
| -------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `type` | `"board"` | **Required.** Must be `"board"`. |
| `group_by` | object | **Required.** Group-by configuration for board columns. See [Group-by configuration](#group-by-configuration). |
| `sub_group_by` | object \| null | Secondary group-by for sub-grouping within columns. Pass `null` to remove. |
| `properties` | array \| null | Property visibility on cards. See [Property configuration](#property-configuration). |
| `cover` | object \| null | Cover image source. See [Cover configuration](#cover-configuration). |
| `cover_size` | `"small"` \| `"medium"` \| `"large"` \| null | Size of the cover image on cards. |
| `cover_aspect` | `"contain"` \| `"cover"` \| null | `"contain"` fits the image; `"cover"` fills the area. |
| `card_layout` | `"list"` \| `"compact"` \| null | `"list"` shows full cards; `"compact"` shows condensed cards. |
### Calendar configuration
```json theme={null}
{
"type": "calendar",
"date_property_id": "DATE_PROPERTY_ID",
"properties": [...],
"view_range": "month",
"show_weekends": true
}
```
| Field | Type | Description |
| ------------------ | ----------------------------- | --------------------------------------------------------------------------------------------- |
| `type` | `"calendar"` | **Required.** Must be `"calendar"`. |
| `date_property_id` | string | **Required.** Property ID of the date property used to position items on the calendar. |
| `properties` | array \| null | Property visibility on calendar cards. See [Property configuration](#property-configuration). |
| `view_range` | `"week"` \| `"month"` \| null | Default calendar range. |
| `show_weekends` | boolean \| null | Whether to show weekend days. |
### Timeline configuration
```json theme={null}
{
"type": "timeline",
"date_property_id": "START_DATE_PROPERTY_ID",
"end_date_property_id": "END_DATE_PROPERTY_ID",
"properties": [...],
"show_table": true,
"table_properties": [...],
"preference": {
"zoom_level": "month",
"center_timestamp": 1706745600000
},
"arrows_by": {
"property_id": "RELATION_PROPERTY_ID"
},
"color_by": true
}
```
| Field | Type | Description |
| ---------------------- | --------------- | --------------------------------------------------------------------------------------------- |
| `type` | `"timeline"` | **Required.** Must be `"timeline"`. |
| `date_property_id` | string | **Required.** Property ID for the start date of timeline items. |
| `end_date_property_id` | string \| null | Property ID for the end date. Pass `null` to clear. |
| `properties` | array \| null | Property visibility on timeline items. See [Property configuration](#property-configuration). |
| `show_table` | boolean \| null | Whether to show the table panel alongside the timeline. |
| `table_properties` | array \| null | Property configuration for the table panel (when `show_table` is true). |
| `preference` | object \| null | Timeline display preferences. See below. |
| `arrows_by` | object \| null | Dependency arrow configuration. See below. |
| `color_by` | boolean \| null | Whether to color timeline items by a property. |
**Timeline preference object:**
| Field | Type | Description |
| ------------------ | ------- | --------------------------------------------------------------------------------------------------------------- |
| `zoom_level` | enum | **Required.** One of: `"hours"`, `"day"`, `"week"`, `"bi_week"`, `"month"`, `"quarter"`, `"year"`, `"5_years"`. |
| `center_timestamp` | integer | Timestamp in milliseconds to center the timeline on. |
**Timeline arrows\_by object:**
| Field | Type | Description |
| ------------- | -------------- | ------------------------------------------------------------------------ |
| `property_id` | string \| null | Relation property ID for dependency arrows, or `null` to disable arrows. |
### Gallery configuration
```json theme={null}
{
"type": "gallery",
"properties": [...],
"cover": { "type": "page_content" },
"cover_size": "large",
"cover_aspect": "cover",
"card_layout": "list"
}
```
| Field | Type | Description |
| -------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `type` | `"gallery"` | **Required.** Must be `"gallery"`. |
| `properties` | array \| null | Property visibility on gallery cards. See [Property configuration](#property-configuration). |
| `cover` | object \| null | Cover image source. See [Cover configuration](#cover-configuration). |
| `cover_size` | `"small"` \| `"medium"` \| `"large"` \| null | Size of the cover image on cards. |
| `cover_aspect` | `"contain"` \| `"cover"` \| null | `"contain"` fits the image; `"cover"` fills the area. |
| `card_layout` | `"list"` \| `"compact"` \| null | `"list"` shows full cards; `"compact"` shows condensed cards. |
### List configuration
```json theme={null}
{
"type": "list",
"properties": [...]
}
```
| Field | Type | Description |
| ------------ | ------------- | ------------------------------------------------------------------------------------------------ |
| `type` | `"list"` | **Required.** Must be `"list"`. |
| `properties` | array \| null | Property visibility and display settings. See [Property configuration](#property-configuration). |
### Map configuration
```json theme={null}
{
"type": "map",
"height": "medium",
"map_by": "LOCATION_PROPERTY_ID",
"properties": [...]
}
```
| Field | Type | Description |
| ------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `type` | `"map"` | **Required.** Must be `"map"`. |
| `height` | `"small"` \| `"medium"` \| `"large"` \| `"extra_large"` \| null | Map display height. Pass `null` to clear. |
| `map_by` | string \| null | Property ID of the location property used to position items on the map. Pass `null` to clear. |
| `properties` | array \| null | Property visibility on map pin cards. See [Property configuration](#property-configuration). |
In responses, an additional read-only field `map_by_property_name` may be present, containing the display name of the `map_by` property.
### Form configuration
```json theme={null}
{
"type": "form",
"is_form_closed": false,
"anonymous_submissions": true,
"submission_permissions": "comment_only"
}
```
| Field | Type | Description |
| ------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `type` | `"form"` | **Required.** Must be `"form"`. |
| `is_form_closed` | boolean \| null | Whether the form is closed for submissions. Pass `null` to clear. |
| `anonymous_submissions` | boolean \| null | Whether anonymous (non-logged-in) submissions are allowed. Pass `null` to clear. |
| `submission_permissions` | `"none"` \| `"comment_only"` \| `"reader"` \| `"read_and_write"` \| `"editor"` \| null | Permission level granted to the submitter on the created page after form submission. Pass `null` to clear. |
### Chart configuration
Chart views display database data as visual charts. The configuration uses a flat object with `chart_type` as a required discriminator. Available fields vary by chart type.
```json theme={null}
{
"type": "chart",
"chart_type": "column",
"x_axis": {
"type": "select",
"property_id": "CATEGORY_PROP_ID",
"sort": { "type": "manual" }
},
"y_axis": {
"aggregator": "sum",
"property_id": "AMOUNT_PROP_ID"
},
"color_theme": "blue",
"color_by_value": true,
"show_data_labels": true,
"height": "medium"
}
```
When `color_by_value` is enabled on a bar or column chart, each bar is shaded along a gradient based on its numeric value — higher values appear in a darker shade and lower values in a lighter shade. This is useful for quickly spotting relative magnitude across categories. Combine with `color_theme` to control the gradient's base color.
**Required fields:**
| Field | Type | Description |
| ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `"chart"` | **Required.** Must be `"chart"`. |
| `chart_type` | `"column"` \| `"bar"` \| `"line"` \| `"donut"` \| `"number"` | **Required.** The chart type: `"column"` (vertical bars), `"bar"` (horizontal bars), `"line"`, `"donut"`, or `"number"` (single value display). |
**Data configuration fields:**
Charts support two data modes: **grouped data** (aggregate values by grouping on a property) and **results** (use raw property values directly).
| Field | Type | Description |
| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x_axis` | object \| null | X-axis grouping configuration for column/bar/line/donut charts using grouped data. Uses the same [group-by configuration](#group-by-configuration) shape. Null when using results mode. Pass `null` to clear. |
| `y_axis` | object \| null | Y-axis [aggregation](#chart-aggregation) for column/bar/line/donut charts using grouped data. Null when using results mode. Pass `null` to clear. |
| `x_axis_property_id` | string \| null | Property ID for x-axis name values when using results (raw property values) mode. Pass `null` to clear. |
| `y_axis_property_id` | string \| null | Property ID for y-axis numeric values when using results mode. Pass `null` to clear. |
| `value` | object \| null | [Aggregation](#chart-aggregation) configuration for number charts (single value display). Pass `null` to clear. |
| `stack_by` | object \| null | Stack-by grouping configuration for stacked/grouped charts (column/bar/line only). Uses the same [group-by configuration](#group-by-configuration) shape. Pass `null` to clear. |
**Format fields (all optional, all nullable):**
| Field | Type | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sort` | `"manual"` \| `"x_ascending"` \| `"x_descending"` \| `"y_ascending"` \| `"y_descending"` | Sort order for chart data. |
| `color_theme` | `"gray"` \| `"blue"` \| `"yellow"` \| `"green"` \| `"purple"` \| `"teal"` \| `"orange"` \| `"pink"` \| `"red"` \| `"auto"` \| `"colorful"` | Color theme. |
| `height` | `"small"` \| `"medium"` \| `"large"` \| `"extra_large"` | Chart height. |
| `hide_empty_groups` | boolean | Whether to hide groups with no data on the x-axis. |
| `legend_position` | `"off"` \| `"bottom"` \| `"side"` | Legend display position. `"off"` hides the legend. |
| `show_data_labels` | boolean | Whether to show data value labels on chart elements. |
| `color_by_value` | boolean | Whether to apply gradient coloring to chart elements based on their numeric value. Higher values appear in a darker shade and lower values in a lighter shade. |
| `axis_labels` | `"none"` \| `"x_axis"` \| `"y_axis"` \| `"both"` | Which axis labels to display. |
| `grid_lines` | `"none"` \| `"horizontal"` \| `"vertical"` \| `"both"` | Which grid lines to display. |
| `y_axis_min` | number \| null | Custom minimum value for the y-axis. |
| `y_axis_max` | number \| null | Custom maximum value for the y-axis. |
| `reference_lines` | array \| null | [Reference lines](#chart-reference-lines) drawn on the chart. |
| `caption` | string \| null | Text caption displayed below the chart. |
**Line-specific fields:**
| Field | Type | Description |
| --------------------- | ------- | ----------------------------------------------- |
| `cumulative` | boolean | Whether to show cumulative values. |
| `smooth_line` | boolean | Whether to use smooth curves. |
| `hide_line_fill_area` | boolean | Whether to hide the shaded area under the line. |
**Bar/column-specific fields:**
| Field | Type | Description |
| ------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `group_style` | `"normal"` \| `"percent"` \| `"side_by_side"` | How grouped/stacked bars are displayed. `"normal"` stacks values, `"percent"` normalizes to 100%, `"side_by_side"` places bars next to each other. |
**Donut-specific fields:**
| Field | Type | Description |
| -------------- | ------------------------------------------------------- | -------------------------------------- |
| `donut_labels` | `"none"` \| `"value"` \| `"name"` \| `"name_and_value"` | What to display on donut chart slices. |
**Number-specific fields:**
| Field | Type | Description |
| ------------ | ------- | -------------------------------- |
| `hide_title` | boolean | Whether to hide the title label. |
#### Chart aggregation
The `y_axis` and `value` fields use an aggregation object:
```json theme={null}
{
"aggregator": "sum",
"property_id": "AMOUNT_PROP_ID"
}
```
| Field | Type | Description |
| ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aggregator` | enum | **Required.** The aggregation operator. `"count"` counts all rows and does not require a `property_id`. All other operators require a `property_id`. |
| `property_id` | string | The property to aggregate on. Required for all operators except `"count"`. |
**Supported aggregation operators:** `count`, `count_values`, `sum`, `average`, `median`, `min`, `max`, `range`, `unique`, `empty`, `not_empty`, `percent_empty`, `percent_not_empty`, `checked`, `unchecked`, `percent_checked`, `percent_unchecked`, `earliest_date`, `latest_date`, `date_range`.
#### Chart reference lines
Reference lines are horizontal lines drawn at specific y-axis values for visual comparison:
```json theme={null}
{
"id": "ref-line-1",
"value": 100,
"label": "Target",
"color": "red",
"dash_style": "dash"
}
```
| Field | Type | Description |
| ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | string | Unique identifier for the reference line. Auto-generated if omitted when creating. |
| `value` | number | **Required.** The y-axis value where the reference line is drawn. |
| `label` | string | **Required.** Label displayed alongside the reference line. |
| `color` | enum | **Required.** Color of the reference line. One of: `"gray"`, `"lightgray"`, `"brown"`, `"yellow"`, `"orange"`, `"green"`, `"blue"`, `"purple"`, `"pink"`, `"red"`. |
| `dash_style` | `"solid"` \| `"dash"` | **Required.** Line style: `"solid"` for a continuous line, `"dash"` for a dashed line. |
### Dashboard configuration
```json theme={null}
{
"type": "dashboard",
"rows": [
{
"id": "row-id-1",
"widgets": [
{ "id": "widget-id-1", "view_id": "VIEW_ID_1", "width": 6, "row_index": 0 },
{ "id": "widget-id-2", "view_id": "VIEW_ID_2", "width": 6, "row_index": 0 }
],
"height": 400
}
]
}
```
| Field | Type | Description |
| ------ | ------------- | ------------------------------------------------------------------------------------------------------- |
| `type` | `"dashboard"` | **Required.** Must be `"dashboard"`. |
| `rows` | array | **Required.** The rows that make up the dashboard layout. Each row contains one or more widget modules. |
**Dashboard row object:**
| Field | Type | Description |
| --------- | ------- | ----------------------------------- |
| `id` | string | The ID of this row module. |
| `widgets` | array | The widget modules within this row. |
| `height` | integer | Fixed height of the row in pixels. |
**Dashboard widget object:**
| Field | Type | Description |
| ----------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `id` | string | The ID of this widget module. |
| `view_id` | string | The ID of the collection view rendered by this widget. |
| `width` | integer | Width of the widget in a 12-column grid (1–12). `12` means full width. |
| `row_index` | integer | The 0-based index of the row this widget belongs to. Widgets in the same row share the same `row_index`. |
Dashboard configuration is **read-only** — it is returned when retrieving a dashboard view but cannot be set directly when creating or updating a view. The layout structure is managed by creating and deleting widget views via the `view_id` parameter on the create endpoint.
### Property configuration
The `properties` array controls which database properties are visible in the view and how they are displayed. Each entry targets a single property by its ID or name.
```json theme={null}
{
"property_id": "abc1",
"visible": true,
"width": 200,
"wrap": true,
"date_format": "relative",
"time_format": "12_hour"
}
```
| Field | Type | Description |
| -------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `property_id` | string | **Required.** The property ID or property name. When a name is provided, the API resolves it to the corresponding property ID. If the string matches both a property ID and a different property's name, the ID match takes priority. |
| `visible` | boolean | Whether the property is visible in this view. |
| `width` | integer (>= 0) | Column width in pixels (table views only). |
| `wrap` | boolean | Whether to wrap content in this property cell or card. |
| `status_show_as` | `"select"` \| `"checkbox"` | How to display status properties. |
| `card_property_width_mode` | `"full_line"` \| `"inline"` | Property width mode in compact card layouts (board/gallery). |
| `date_format` | enum | Display format for date properties. One of: `"full"`, `"short"`, `"month_day_year"`, `"day_month_year"`, `"year_month_day"`, `"relative"`. |
| `time_format` | enum | Time display format for date properties. One of: `"12_hour"`, `"24_hour"`, `"hidden"`. |
### Group-by configuration
Group-by lets you organize rows or cards into sections based on a property's values. The shape varies by property type, forming a discriminated union on the `type` field.
All group-by variants share these fields:
| Field | Type | Description |
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `type` | string | **Required.** The property type being grouped. Determines which additional fields are available. |
| `property_id` | string | **Required.** The property ID to group by. |
| `sort` | object | **Required.** Sort order for the groups. An object with `type`: `"manual"`, `"ascending"`, or `"descending"`. |
| `hide_empty_groups` | boolean | Whether to hide groups with no items. |
The following table shows which `type` values are supported and what extra fields each variant accepts:
| `type` value(s) | Extra required fields | Extra optional fields |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `select`, `multi_select` | — | — |
| `status` | `group_by`: `"group"` (by status group: To Do/In Progress/Done) or `"option"` (by individual option) | — |
| `person`, `created_by`, `last_edited_by` | — | — |
| `relation` | — | — |
| `date`, `created_time`, `last_edited_time` | `group_by`: `"relative"` \| `"day"` \| `"week"` \| `"month"` \| `"year"` | `start_day_of_week`: `0` (Sunday) or `1` (Monday) |
| `text`, `title`, `url`, `email`, `phone_number` | `group_by`: `"exact"` or `"alphabet_prefix"` (first letter) | — |
| `number` | — | `range_start`, `range_end`, `range_size` (>= 1) for bucket grouping |
| `checkbox` | — | — |
| `formula` | `group_by`: a nested sub-group-by object (see below) | — |
**Formula group-by** uses a nested `group_by` object that describes how to group the formula's result type. The nested object does not include `property_id` (it inherits from the parent). Supported formula result types:
| Result type | Nested `group_by` fields |
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| `date` | `type`, `group_by` (`"relative"` \| `"day"` \| `"week"` \| `"month"` \| `"year"`), `sort`, optionally `start_day_of_week` |
| `text` | `type`, `group_by` (`"exact"` \| `"alphabet_prefix"`), `sort` |
| `number` | `type`, `sort`, optionally `range_start`, `range_end`, `range_size` |
| `checkbox` | `type`, `sort` |
```json Group by select example theme={null}
{
"type": "select",
"property_id": "PRIORITY_PROPERTY_ID",
"sort": { "type": "manual" },
"hide_empty_groups": true
}
```
```json Group by status example theme={null}
{
"type": "status",
"property_id": "STATUS_PROPERTY_ID",
"group_by": "group",
"sort": { "type": "ascending" }
}
```
```json Group by date example theme={null}
{
"type": "date",
"property_id": "DUE_DATE_PROPERTY_ID",
"group_by": "week",
"sort": { "type": "ascending" },
"start_day_of_week": 1
}
```
```json Group by formula example theme={null}
{
"type": "formula",
"property_id": "FORMULA_PROPERTY_ID",
"group_by": {
"type": "number",
"sort": { "type": "ascending" },
"range_start": 0,
"range_end": 100,
"range_size": 10
}
}
```
### Subtask configuration
Subtask (sub-item) configuration controls how parent-child relationships are displayed in table views. This uses a self-referencing relation property to establish hierarchy.
```json theme={null}
{
"property_id": "RELATION_PROPERTY_ID",
"display_mode": "show",
"filter_scope": "parents_and_subitems",
"toggle_column_id": "title"
}
```
| Field | Type | Description |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `property_id` | string | Relation property ID used for parent-child nesting. |
| `display_mode` | enum | How sub-items are displayed. One of: `"show"` (hierarchical with toggles), `"hidden"` (parents with a count), `"flattened"` (sub-items with a parent indicator), `"disabled"` (no sub-item rendering). |
| `filter_scope` | enum | Which items are included when filtering. One of: `"parents"` (parent items only), `"parents_and_subitems"` (both), `"subitems"` (sub-items only). |
| `toggle_column_id` | string | Property ID of the column showing the expand/collapse toggle. |
### Cover configuration
Cover configuration controls the image displayed at the top of each card in board and gallery views.
```json theme={null}
{
"type": "page_cover",
}
```
| Field | Type | Description |
| ------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | enum | **Required.** Source of the cover image. One of: `"page_cover"` (the page's cover image), `"page_content"` (first image in page content), `"property"` (an image from a file property). |
| `property_id` | string | Property ID to use as the cover image source. Only used when `type` is `"property"`. |
### Clearing configuration with null
When updating a view, you can pass `null` for any nullable configuration field to remove that setting. Only include the fields you want to change — omitted fields are left unchanged.
Here are common scenarios:
```javascript Remove grouping from a table expandable theme={null}
// A table view currently has group_by set.
// Pass null to remove grouping and return to a flat table.
const updated = await notion.views.update({
view_id: "VIEW_ID",
configuration: {
type: "table",
group_by: null,
},
});
```
```javascript Remove cover images from a board expandable theme={null}
// A board view currently shows cover images.
// Pass null for cover-related fields to remove them.
const updated = await notion.views.update({
view_id: "VIEW_ID",
configuration: {
type: "board",
group_by: {
type: "status",
property_id: "STATUS_PROP_ID",
group_by: "group",
sort: { type: "manual" },
},
cover: null,
cover_size: null,
cover_aspect: null,
},
});
```
```javascript Disable subtasks on a table expandable theme={null}
// Explicitly disable subtask rendering on a table view.
// Note: passing subtasks: null resets to defaults (which may
// still show subtasks). Use display_mode: "disabled" instead.
const updated = await notion.views.update({
view_id: "VIEW_ID",
configuration: {
type: "table",
subtasks: { display_mode: "disabled" },
},
});
```
```javascript Remove dependency arrows from a timeline expandable theme={null}
// A timeline view currently shows dependency arrows.
// Pass null for arrows_by to remove them.
const updated = await notion.views.update({
view_id: "VIEW_ID",
configuration: {
type: "timeline",
date_property_id: "START_DATE_PROP_ID",
arrows_by: null,
},
});
```
```javascript Clear a view's filter and sorts expandable theme={null}
// Clear the top-level filter and sorts (not inside configuration).
const updated = await notion.views.update({
view_id: "VIEW_ID",
filter: null,
sorts: null,
});
```
Configuration updates use **shallow merge** — only the fields you include are changed, and omitted optional fields are preserved. The `configuration` field itself is optional (omit it to leave config unchanged). When present, you must include `type` and any fields marked as required for that view type (e.g., board views always require `group_by`, calendar/timeline views always require `date_property_id`). See [Feature support by view type](#feature-support-by-view-type) for which fields are required vs optional per view type.
## Quick filters
Quick filters appear in the view's filter bar and let users quickly toggle property-level filters without opening the full filter panel. In the API, `quick_filters` is a map where keys are property names or IDs, and values are filter conditions using the same shape as [property filters](/reference/filter-data-source-entries) but without the `property` field.
People filters accept `"me"` as a value for `contains` and `does_not_contain` to match the current user, so you can create filters like "assigned to me" without hardcoding a user ID.
### Adding quick filters on create
```javascript JavaScript expandable theme={null}
const view = await notion.views.create({
database_id: "DATABASE_ID",
data_source_id: "DATA_SOURCE_ID",
name: "Active tasks",
type: "table",
quick_filters: {
"Status": {
status: { equals: "In progress" },
},
"Priority": {
select: { equals: "High" },
},
},
});
```
### Adding or updating a quick filter
To add a new quick filter or update an existing one, include the property key with the new filter condition. Other existing quick filters are preserved.
```javascript JavaScript expandable theme={null}
const view = await notion.views.update({
view_id: "VIEW_ID",
quick_filters: {
"Assignee": {
people: { contains: "me" },
},
},
});
```
### Removing a quick filter
Set a specific quick filter to `null` to remove it from the filter bar. Other quick filters are preserved.
```javascript JavaScript expandable theme={null}
const view = await notion.views.update({
view_id: "VIEW_ID",
quick_filters: {
"Status": null,
},
});
```
### Clearing all quick filters
Set the entire `quick_filters` field to `null` to remove all quick filters from the view.
```javascript JavaScript expandable theme={null}
const view = await notion.views.update({
view_id: "VIEW_ID",
quick_filters: null,
});
```
## Dashboard views
Dashboard views let you arrange multiple widget views in a grid layout on a single database. Each widget is itself a view (table, board, list, etc.) that can reference a different data source.
### Creating a dashboard
Create a dashboard view the same way as any other view — pass `type: "dashboard"` with a `database_id`:
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2025-09-03" \
--data '{
"database_id": "DATABASE_ID",
"data_source_id": "DATA_SOURCE_ID",
"name": "Project overview",
"type": "dashboard"
}'
```
```javascript JavaScript expandable theme={null}
const dashboard = await notion.views.create({
database_id: "DATABASE_ID",
data_source_id: "DATA_SOURCE_ID",
name: "Project overview",
type: "dashboard",
});
console.log(dashboard.id); // The dashboard view's ID
```
### Adding widget views
To add a widget to a dashboard, create a view with `view_id` set to the dashboard's ID instead of `database_id`. Each widget can use a different `data_source_id`. Dashboards support all view types as widgets except for other dashboards (no nested dashboards).
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2025-09-03" \
--data '{
"view_id": "DASHBOARD_VIEW_ID",
"data_source_id": "DATA_SOURCE_ID",
"name": "Tasks by status",
"type": "board",
"configuration": {
"type": "board",
"group_by": {
"type": "status",
"property_id": "STATUS_PROPERTY_ID",
"group_by": "group",
"sort": { "type": "manual" }
}
}
}'
```
```javascript JavaScript expandable theme={null}
const widget = await notion.views.create({
view_id: "DASHBOARD_VIEW_ID",
data_source_id: "DATA_SOURCE_ID",
name: "Tasks by status",
type: "board",
configuration: {
type: "board",
group_by: {
type: "status",
property_id: "STATUS_PROPERTY_ID",
group_by: "group",
sort: { type: "manual" },
},
},
});
console.log(widget.id); // The widget view's ID
console.log(widget.dashboard_view_id); // The parent dashboard's ID
```
### Widget placement
When adding a widget to a dashboard, you can control where it appears in the layout using the `placement` parameter. This is a discriminated union on the `type` field:
| Variant | Fields | Description |
| -------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `new_row` | `type`, optional `row_index` | Creates a new row containing the widget. If `row_index` is omitted, the new row is appended at the end. If provided, the new row is inserted at that 0-based position. |
| `existing_row` | `type`, `row_index` (required) | Adds the widget side-by-side to an existing row at the specified 0-based index. Column widths are automatically redistributed. |
```json Append a new row (default) theme={null}
{
"placement": { "type": "new_row" }
}
```
```json Insert a new row at the top theme={null}
{
"placement": { "type": "new_row", "row_index": 0 }
}
```
```json Add to an existing row theme={null}
{
"placement": { "type": "existing_row", "row_index": 2 }
}
```
The `placement` parameter is only valid when `view_id` is provided (dashboard widget creation). It cannot be used with `database_id`. Each dashboard row supports a maximum of 4 widgets.
### Retrieving a dashboard
When you retrieve a dashboard view, its `configuration` contains the full layout structure — rows of widgets with their positions and sizes:
```json theme={null}
{
"object": "view",
"id": "DASHBOARD_VIEW_ID",
"type": "dashboard",
"configuration": {
"type": "dashboard",
"rows": [
{
"id": "row-1",
"widgets": [
{ "id": "widget-1", "view_id": "VIEW_ID_1", "width": 6, "row_index": 0 },
{ "id": "widget-2", "view_id": "VIEW_ID_2", "width": 6, "row_index": 0 }
]
}
]
}
}
```
Widget views include a `dashboard_view_id` field that references their parent dashboard. Their `parent.database_id` always resolves to the underlying database, even though they are positioned inside a dashboard layout.
### Deleting widget views
Delete a widget view using the standard delete endpoint. This also removes the widget from the dashboard's layout structure.
Dashboard views cannot be nested — you cannot create a dashboard widget inside another dashboard.
## Querying a view
Use a view query to fetch pages using the view's saved filter and sort configuration. This lets connections reproduce what a user sees in the Notion UI for a particular view, without needing to manually reconstruct the filter/sort logic.
View queries use a three-step pattern:
1. **Create a query** — executes the view's filters/sorts and returns the first page of results along with a `query_id`.
2. **Paginate results** — use the `query_id` to fetch additional pages from the cached result set.
3. **Delete the query** (recommended) — free the cached result set when you're done paginating.
### Step 1: Create a view query
```bash cURL theme={null}
curl -X POST https://api.notion.com/v1/views/VIEW_ID/queries \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"page_size": 50
}'
```
```javascript JavaScript theme={null}
const query = await notion.views.queries.create({
view_id: "VIEW_ID",
page_size: 50,
});
console.log(query.id); // Query ID for pagination
console.log(query.total_count); // Total matching pages
console.log(query.results); // First page of results
console.log(query.has_more); // Whether more pages exist
```
The response includes the first page of results inline:
```json theme={null}
{
"object": "view_query",
"id": "query-id-here",
"view_id": "VIEW_ID",
"expires_at": "2026-01-20T14:37:00.000Z",
"total_count": 128,
"results": [
{ "object": "page", "id": "..." }
],
"next_cursor": "cursor-string",
"has_more": true
}
```
### Step 2: Paginate results
```bash cURL theme={null}
curl -X GET "https://api.notion.com/v1/views/VIEW_ID/queries/QUERY_ID?start_cursor=CURSOR&page_size=50" \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
const nextPage = await notion.views.queries.results({
view_id: "VIEW_ID",
query_id: "QUERY_ID",
start_cursor: "CURSOR",
page_size: 50,
});
```
### Step 3: Delete the query (recommended)
Once you've finished paginating, delete the query to free the cached result set. This is optional — queries expire automatically after approximately 15 minutes — but recommended as good practice, especially if your connection runs queries frequently.
```bash cURL theme={null}
curl -X DELETE "https://api.notion.com/v1/views/VIEW_ID/queries/QUERY_ID" \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
await notion.views.queries.delete({
view_id: "VIEW_ID",
query_id: "QUERY_ID",
});
```
The response confirms deletion:
```json theme={null}
{
"object": "view_query",
"id": "QUERY_ID",
"deleted": true
}
```
This endpoint is idempotent — calling it on an already-deleted or expired query still returns success.
**Query expiration**
Cached query results expire after a short TTL (approximately 15 minutes). If a query expires, create a new one. This caching approach provides stable pagination — results won't shift between pages due to concurrent data changes.
View queries do not support stacking additional filters or sorts on top of the saved view definition. If you need different filter/sort criteria, create a new view (or update an existing one) and query that instead.
## Permissions
View endpoints reuse existing database [connection capabilities](/reference/capabilities):
| Operation | Required capability |
| --------------------------------------- | --------------------------------------------------------------------------- |
| List views | `read_content` or `read_property` |
| Retrieve a view | `read_content` or `read_property` |
| Create a view | `insert_content`, `insert_property`, `update_content`, or `update_property` |
| Update a view | `update_content` or `update_property` |
| Delete a view | `update_content` or `update_property` |
| Query a view (create, paginate, delete) | `read_content` or `read_property` |
The connection must also have access to the parent database. If it doesn't, the API returns a `404` rather than a `403`.
## Next steps
* Explore the [database object](/reference/database) and [data source object](/reference/data-source) reference docs for the parent resources that views live under.
* Learn about [filters](/reference/filter-data-source-entries) and [sorts](/reference/sort-data-source-entries) — these shapes are shared between data source queries and view configuration.
* Review [Working with databases](/guides/data-apis/working-with-databases) for a broader overview of database concepts.
* See [Preparing your connection for users](/guides/get-started/preparing-for-users) to learn how to set up databases, views, and pages automatically when users install your connection.
# Authorization
Source: https://developers.notion.com/guides/get-started/authorization
This guide describes the authorization flows for Notion connections and personal access tokens.
## What is authorization?
Authorization is the process of granting a connection or token access to Notion data. [Internal connections](/guides/get-started/internal-connections) use a static API token, [personal access tokens](/guides/get-started/personal-access-tokens) use a user-scoped static API token, and [public connections](/guides/get-started/public-connections) use the [OAuth 2.0](https://oauth.net/2/) protocol.
## Internal connection auth flow set-up
To use an internal connection, start by creating your connection in the Developer portal.
The internal connection will be associated with the workspace of your choice. You are required to be a workspace owner to create a connection.
Once the connection is created, you can update its settings as needed under the `Configuration` tab and retrieve the installation access token in this tab.
The installation access token will be used to authenticate REST API requests. The connection sends the same token in every API request.
### Connection permissions
Before a connection can interact with your Notion workspace page(s), the page must be manually shared with the connection. To share a page with a connection, visit the page in your Notion workspace, click the ••• menu at the top right of a page, scroll down to `Add connections`, and use the search bar to find and select the connection from the dropdown list.
Once the connection is shared, you can start making API requests. If the page is not shared, any API requests made will respond with an error.
**Never share your installation access token**
Your installation access token is a secret. To keep your connection secure, never store the token in your source code or commit it in version control. Instead, read the token from an environment variable. Use a secret manager or deployment system to set the token in the environment.
[Learn more: Best Practices for Handling API Keys](/guides/get-started/handling-api-keys)
### Making API requests with an internal connection
Any time your connection interacts with your workspace, include the installation access token in the `Authorization` header with every API request. However, if you are using Notion’s [SDK for JavaScript](https://github.com/makenotion/notion-sdk-js) to interact with the REST API, the token is set once when a client is initialized.
```http HTTP theme={null}
GET /v1/pages/b55c9c91-384d-452b-81db-d1ef79372b75 HTTP/1.1
Authorization: Bearer {INTEGRATION_TOKEN}
```
```javascript JavaScript theme={null}
const { Client } = require("@notionhq/client")
// Initializing a client
const notion = new Client({
auth: process.env.NOTION_TOKEN,
})
const getUsers = async () => {
const listUsersResponse = await notion.users.list({})
}
```
If you are not using the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js), also set the [`Notion-Version`](/reference/versioning) and [`Content-type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) headers in all of your requests, like so:
```json JSON theme={null}
headers: {
Authorization: `Bearer ${process.env.NOTION_TOKEN}`,
"Notion-Version": "2026-03-11",
"Content-Type": "application/json",
},
```
If you receive an error response from the API, check if the connection has been properly [added to the page](https://www.notion.com/help/add-and-manage-connections-with-the-api#manage-connections-in-your-workspace). If this does not solve the problem, refer to our [Status codes](/reference/status-codes) page for more information.
## Personal access token authentication
Personal access tokens (PATs) are created directly by a Notion user in the Developer portal. There is no OAuth flow and no page picker. A PAT acts as the user who created it and uses that user's permissions in the selected workspace.
Use a PAT when a script, CLI workflow, Worker, or trusted tool should act as you. Use an internal connection for team-owned workspace automations, or a public connection when other Notion users need to install your app.
PATs use the same `Authorization` header as other Notion API tokens:
```http HTTP theme={null}
GET /v1/users/me HTTP/1.1
Authorization: Bearer {PERSONAL_ACCESS_TOKEN}
Notion-Version: 2026-03-11
```
See [Personal access tokens](/guides/get-started/personal-access-tokens) for creation steps, workspace admin controls, tier defaults, and security guidance.
## Public connection auth flow set-up
A public connection can be installed in any Notion workspace within its [installation scope](/guides/get-started/public-connections#installation-scope) — either any workspace, or a specific set chosen at creation time.
Since a public connection is not tied to a single workspace with a single installation access token, public connections instead follow the [OAuth 2.0 protocol](https://oauth.net/2/) to authorize a connection to interact with a workspace.
### How to make a public connection
Navigate to the Developer portal and create a new public connection.
Fill out the form with your connection details, including your redirect URI(s) under the OAuth configuration section and the connection's [installation scope](/guides/get-started/public-connections#installation-scope) — either **Any workspace** or **Selected workspaces only**. This can't be changed after creation.
The redirect URI is the URI your users will be redirected to after authorizing the public connection. To learn more, read [OAuth’s description of redirect URIs](https://www.oauth.com/oauth2-servers/redirect-uris/).
Marketplace listing details (such as descriptions, categories, and images) are managed separately through the **Listings** section. Refer to the [List on the Marketplace](/guides/get-started/marketplace-listing) guide to learn more.
### Public connection authorization overview
Once your connection has been made public, you can update your connection code to use the public auth flow.
As an overview, the authorization flow includes the following steps. Each step will be described in more detail below.
Navigate the user to the connection’s authorization URL. This URL is provided in the Developer portal.
After the user selects which workspace pages to share, Notion redirects the user to the connection’s redirect URI and includes a `code` query parameter. The redirect URI is the one you specified in your Developer portal.
Make a `POST` request to [create an access token](/reference/create-a-token), which exchanges the temporary `code` for an access token.
The Notion API responds with an access token and some additional information.
Store the access token for future API requests. View the [API reference docs](/reference/intro) to learn about available endpoints.
### Step 1 - Navigate the user to the connection’s authorization URL
After creating a public connection in the Developer portal, access the connection’s secrets in the **Configuration** tab. Similarly to the internal connections, these values should be protected and should never be included in source code or version control.
As an example, your `.env` file using these secrets could look like this:
```shell Shell theme={null}
#.env
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
NOTION_AUTH_URL=
```
To start the authorization flow for a public connection, direct the prospective user to the authorization URL. A common pattern is to include a hyperlink in the connection app that interacts with the Notion REST API. For example, an app that lets users create Notion pages in their workspaces should send users to the authorization URL first.
The following example shows an authorization URL made available through a hyperlink:
```html HTML theme={null}
Add to Notion
```
The URL begins with `https://api.notion.com/v1/oauth/authorize` and has the following parameters:
| Parameter | Description | Required |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- |
| `client_id` | An identifier for your connection, found in the connection settings. | ✅ |
| `redirect_uri` | The URL where the user should return after granting access. | ✅ |
| `response_type` | Always use `code`. | ✅ |
| `owner` | Always use `user`. | ✅ |
| `state` | If the user was in the middle of an interaction or operation, then this parameter can be used to restore state after the user returns. It can also be used to prevent CSRF attacks. | |
Once the authorization URL is visited, the user will be shown a prompt that varies depending on whether or not the connection comes with a Notion template option.
#### Prompt for a standard connection with no template option (Default)
In the standard connection permissions flow, a prompt describes the connection [capabilities](/reference/capabilities), presented to the user as what the connection would like to be able to do in the workspace. A user can either select pages to grant the connection access to, or cancel the request.
If the user presses **Cancel**, they will be redirected to the redirect URI with and `error` query param added.
```
www.example.com/my-redirect-uri?error=access_denied&state=
```
You can use this `error`query parameter to conditionally update your app’s state as needed.
If the user opts to `Select pages`, then a page picker interface opens. A user can search for and select pages and databases to share with the connection from the page picker.
The page picker only displays pages or databases to which a user has [full access](https://www.notion.com/help/sharing-and-permissions), because a user needs full access to a resource in order to be able to share it with a connection.
Users can select which pages to give the connection access to, including both private and public pages available to them. Parent pages can be selected to quickly provide access to child pages, as giving access to a parent page will provide access to all available child pages. Users can return to this view at a later time to update access settings if circumstances change.
If the user clicks `Allow access`, they are then redirected to the `redirect_uri` with a temporary authorization `code`. If the user denies access, they are redirected to the `redirect_uri` with an `error` query parameter.
If the user clicks `Allow access` and the rest of the auth flow is not completed, the connection will *not* have access to the pages that were selected.
#### Prompt for a connection with a Notion template option
Public connections offer the option of providing a public Notion page to use as a template during the auth flow.
To add a template to your workspace, complete the following steps:
* Choose a public page in your workspace that you want users to be able to duplicate.
* Navigate to your connection in the Developer portal and open the **Configuration** tab, then scroll to the Basic information section.
* Scroll to the bottom of your distribution settings and add the URL of the Notion page you selected to the **Notion URL for optional template** input.
Once this URL is added, your auth flow prompt appearance will be updated.
Going back to your prompt view, if the connection offers a Notion template option, the first step in the permissions flow will describe the connection [capabilities](/reference/capabilities). This is presented to the user as what the connection would be able to do in the workspace, and it prompts the user to click `Next`.
In the next step, a user can either choose to duplicate the template that you provided or to select existing pages to share with the connection.
If the user chooses to duplicate the template, then the following happens automatically:
* The connection is added to the user’s workspace.
* The template is duplicated as a new page in the workspace.
* The new page is shared with the connection.
If the user chooses to select pages to share with the connection, then they continue to the page picker interface that’s part of the [prompt for a standard connection](#prompt-for-a-standard-connection-with-no-template-option-default).
After a user authorizes a public connection, only that user is able to interact or share pages and databases with the connection. Unlike internal connections, if multiple members in a workspace want to use a public connection, each prospective user needs to individually follow the auth flow for the connection.
**User authorization failures**
User authorization failures can happen. If a user chooses to `Cancel` the request, then a failure is triggered. Build your connection to handle these cases gracefully, as needed.
In some cases, Notion redirects the user to the `redirect_uri` that you set up when you created the public connection, along with an `error` query parameter. Notion uses the common [error codes in the OAuth specification](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1). Use the `error` code to create a helpful prompt for the user when they’re redirected here.
### Step 2 - Notion redirects the user to the connection’s redirect URI and includes a `code` parameter
When you first created the public connection, you specified a redirect URI. If the user follows the prompt to `Allow access` for the connection, then Notion generates a temporary `code` and sends a request to the redirect URI with the following information in the query string:
| Parameter | Description | Required |
| :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :------- |
| `code` | A temporary authorization code. | ✅ |
| `state` | The value provided by the connection when the user was [prompted for access](#prompt-for-a-standard-connection-with-no-template-option-default). | |
To complete the next step, retrieve the `code` query parameter provided in the redirect. The retrieval method varies depending on your app’s tech stack.
In a React component, for example, the query parameters are made available through the `useRouter()` hook:
```javascript JavaScript theme={null}
export default function AuthRedirectPage() {
const router = useRouter();
const { code } = router.query;
...
}
```
### Step 3 - Send the `code` in a `POST` request to the Notion API
The connection needs to exchange the temporary `code` for an `access_token`.
To set up this step, retrieve the `code` from the redirect URI.
Next, send the `code` as part of a `POST` request to Notion’s token endpoint: [https://api.notion.com/v1/oauth/token](https://api.notion.com/v1/oauth/token).
This endpoint is described in more detail in the API reference docs for [creating a token](/reference/create-a-token).
The request is authorized using HTTP Basic Authentication. The credential is a colon-delimited combination of the connection’s `CLIENT_ID` and `CLIENT_SECRET`, like so:
```bash theme={null}
CLIENT_ID:CLIENT_SECRET
```
Find both of these values in the Developer portal.
Note that in [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication), credentials are `base64` encoded before being added to the `Authorization` header.
The body of the request contains the following JSON-encoded fields:
| Field | Type | Description | Required |
| :--------------- | :------- | :----------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"grant_type"` | `string` | Always use `"authorization_code"`. | ✅ |
| `"code"` | `string` | The temporary authorization code received in the incoming request to the `"redirect_uri"`. | ✅ |
| `"redirect_uri"` | `string` | The `"redirect_uri"` that was provided in the Authorization step. | ✅/❌\*
\* If the redirect URI was supplied as a query param in the Authorization URL, this field is required. If there are more than one redirect URIs included in your connection settings, this field is required. Otherwise, it is not allowed. Learn more in the [Create a token page](/reference/create-a-token). |
The following is an example request to exchange the authorization code for an access token:
```http HTTP theme={null}
POST /v1/oauth/token HTTP/1.1
Authorization: Basic "$CLIENT_ID:$CLIENT_SECRET"
Content-Type: application/json
{"grant_type":"authorization_code","code":"e202e8c9-0990-40af-855f-ff8f872b1ec6", "redirect_uri":"https://example.com/auth/notion/callback"}
```
The Node-equivalent of this example would look something like this:
```javascript JavaScript theme={null}
...
const clientId = process.env.OAUTH_CLIENT_ID;
const clientSecret = process.env.OAUTH_CLIENT_SECRET;
const redirectUri = process.env.OAUTH_REDIRECT_URI;
// encode in base 64
const encoded = btoa(`${clientId}:${clientSecret}`);
const response = await fetch("https://api.notion.com/v1/oauth/token", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Basic ${encoded}`,
},
body: JSON.stringify({
grant_type: "authorization_code",
code: "your-temporary-code",
redirect_uri: redirectUri,
}),
});
...
```
### Step 4 - Notion responds with an `access_token` , `refresh_token`, and additional information
Notion responds to the request with an `access_token`, `refresh_token`, and additional information. The `access_token` will be used to authenticate subsequent Notion REST API requests. The `refresh_token` will be used to refresh the access token, which generates a new `access_token`.
The response contains the following JSON-encoded fields:
| Field | Type | Description | Not null |
| :------------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- |
| `"access_token"` | `string` | An access token used to authorize requests to the Notion API. | ✅ |
| `"refresh_token"` | `string` | A refresh token used to generate a new access token | ✅ |
| `"bot_id"` | `string` | An identifier for this authorization. | ✅ |
| `"duplicated_template_id"` | `string` | The ID of the new page created in the user’s workspace. The new page is a duplicate of the template that the developer provided with the connection. If the developer didn’t provide a template for the connection, then the value is `null`. | |
| `"owner"` | `object` | An object containing information about who can view and share this connection. A [user object](/reference/user) is returned, representing the user who authorized the connection. | ✅ |
| `"workspace_icon"` | `string` | A URL to an image that can be used to display this authorization in the UI. | |
| `"workspace_id"` | `string` | The ID of the workspace where this authorization took place. | ✅ |
| `"workspace_name"` | `string` | A human-readable name that can be used to display this authorization in the UI. | |
**Token request failures**
If something goes wrong when the connection attempts to exchange the `code` for an `access_token`, then the response contains a JSON-encoded body with an `"error"` field. Notion uses the common [error codes from the OAuth specification](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2).
### Step 5 - The connection stores the `access_token` and `refresh_token` for future requests
Set up a way for your connection to store both the `access_token` and `refresh_token` that it receives. The `access_token` is used to make authorized requests to the Notion API, and the `refresh_token` is used to generate a new `access_token`.
**Tips for storing and using token access**
* Setting up a database is a typical solution for storing access tokens. If you’re using a database, then build relations between an `access_token`, `refresh_token`, and the corresponding Notion resources that your connection accesses with that token. For example, if you store a Notion database or page ID, relate those records with the correct `access_token` that you use to authorize requests to read or write to that database or page, and the `refresh_token` for ongoing token lifecycle support..
* Store all of the information that your connection receives with the `access_token` and `refresh_token`. You never know when your UI or product requirements might change and you’ll need this data. It's really hard (or impossible) to send users to repeat the authorization flow to generate the information again.
* The `bot_id` returned along with your tokens identifies the Notion bot for the authorization. Store the token pair from each successful authorization response. This includes re-authorization of the same connection, where Notion may return a new `access_token` and `refresh_token`.
### Step 6 - Refreshing an access token
Refreshing an access token will generate a new access token and a new refresh token.
Send the `refresh_token` provided from [Step 4](#step-4-notion-responds-with-an-access_token--refresh_token-and-additional-information) as part of a `POST` request to Notion’s token endpoint: [https://api.notion.com/v1/oauth/token](https://api.notion.com/v1/oauth/token).
This endpoint is described in more detail in the API reference docs for [refreshing a token](/reference/refresh-a-token).
The request is authorized using HTTP Basic Authentication. The credential is a colon-delimited combination of the connection’s `CLIENT_ID` and `CLIENT_SECRET`, like so:
```bash theme={null}
CLIENT_ID:CLIENT_SECRET
```
Find both of these values in the Developer portal.
Note that in [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication), credentials are `base64` encoded before being added to the `Authorization` header.
The body of the request contains the following JSON-encoded fields:
| Field | Type | Description | Required |
| :---------------- | :------- | :-------------------------------------------------------- | :------- |
| `"grant_type"` | `string` | Always use `"refresh_token"`. | ✅ |
| `"refresh_token"` | `string` | The `"refresh_token"` returned in the Authorization step. | ✅ |
The following is an example request to exchange the `refresh_token` for a new access token and new refresh token
```http HTTP theme={null}
POST /v1/oauth/token HTTP/1.1
Authorization: Basic "$CLIENT_ID:$CLIENT_SECRET"
Content-Type: application/json
{"grant_type":"refresh_token","refresh_token":"nrt_4991090011501Ejc6Xn4sHguI7jZIN449mKe9PRhpMfNK"}
```
The Node-equivalent of this example would look something like this:
```javascript JavaScript theme={null}
...
const clientId = process.env.OAUTH_CLIENT_ID;
const clientSecret = process.env.OAUTH_CLIENT_SECRET;
// encode in base 64
const encoded = btoa(`${clientId}:${clientSecret}`);
const response = await fetch("https://api.notion.com/v1/oauth/token", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Basic ${encoded}`,
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: "your-refresh-token",
}),
});
...
```
# Secure API tokens
Source: https://developers.notion.com/guides/get-started/handling-api-keys
Learn how to manage and secure your Notion API tokens.
Notion API tokens authorize requests to the Notion API. This guidance applies to internal connection tokens, OAuth access tokens, and [personal access tokens](/guides/get-started/personal-access-tokens).
## Protect your tokens
Anyone who obtains a token can make requests allowed by its capabilities and content permissions. Depending on the token, this could expose or change workspace content.
For a PAT, access is limited by the token's capabilities and the permissions of the person who created it. For an internal connection, access is limited to pages shared with the connection and its capabilities.
* Don't share tokens in messages, support requests, issue trackers, or other public places.
* Don't put tokens directly in source code or configuration files.
* Store tokens in environment variables or an encrypted secret manager.
* Use separate tokens for development, staging, and production.
* Grant only the capabilities that each application needs.
* Revoke tokens that you no longer use.
## Store tokens outside your code
Use an environment variable for local development. Add `.env` files to `.gitignore` and never commit them.
```bash theme={null}
# .env file (never commit this file)
NOTION_API_KEY=ntn_abc123def456ghi789jkl012mno345pqr
```
```typescript TypeScript theme={null}
const notion = new Client({
auth: process.env.NOTION_API_KEY,
});
```
Use a secret manager for deployed applications. Limit who can read production secrets and keep an inventory of each token's owner and purpose.
## Scan for exposed tokens
Enable secret scanning in your repository and CI system. Block commits that contain tokens, and alert the token owner if a token is detected.
## Replace tokens regularly
Replace long-lived tokens on a schedule and whenever someone with access to them leaves your team. PATs expire on the date chosen when the token is created, up to one year later. Replace a PAT before it expires.
## Respond to an exposed token
Revoke or replace a token immediately if it may have been exposed.
### 1. Disable the token
Log in to Notion.
Go to **Settings** → **Connections** → **Develop or manage connections**.
Find the connection or personal access token that uses the exposed token.
Select **Refresh** for an internal connection, or revoke the personal access token.
For a PAT, you can also revoke the token from Personal access tokens in the Developer portal. Create a new token if the application still needs access.
### 2. Update applications
Replace the old token in every application and environment that used it. Test the new token, and remove the old value from configuration files and documentation.
### 3. Review activity
Check recent changes to pages and databases. Look for connections you don't recognize in **Settings** → **Connections**.
## Getting help
If you need help with an exposed token or unauthorized access, contact [Notion support](https://www.notion.com/help).
# Internal connections
Source: https://developers.notion.com/guides/get-started/internal-connections
Learn how internal connections work, how permissions are managed, and how to create one.
## What is an internal connection?
An internal connection is scoped to a single Notion workspace. Only members of that workspace can use it. Internal connections are ideal for team-owned automations and workflows — things like syncing data from external tools, sending notifications when pages change, or powering internal dashboards.
Internal connections use a static API token for authentication. There's no OAuth flow to implement — you get a token immediately when you create the connection, and you use that same token for every API request.
If you want a token that acts as your own Notion user for a script, CLI workflow, Worker, or trusted tool, use a [personal access token](/guides/get-started/personal-access-tokens) instead. PATs use the creator's page permissions instead of a separate bot's page permissions.
In this guide, you'll learn:
* How internal connection permissions work (and how they differ from public connections)
* How to create an internal connection and share pages with it
* How to authenticate API requests using your installation access token
## How permissions work
An internal connection operates as its own **bot user**. It is not tied to any specific workspace member. This means:
* **Permissions belong to the connection, not to a person.** When a page is shared with the connection, the connection itself has access — regardless of which workspace member shared it.
* **Access is inherited.** Sharing a parent page with the connection grants access to all of its child pages as well.
* **Access persists independently of users.** If the user who shared a page leaves the workspace, the connection retains access to that page.
* **Any Workspace Owner can see the connection.** All internal connections are visible in the Developer portal to every Workspace Owner in the workspace, including connections created by others.
This is one of the biggest differences from [public connections](/guides/get-started/public-connections), where the connection acts on behalf of the individual user who authorized it, and [personal access tokens](/guides/get-started/personal-access-tokens), which act as the user who created the token.
## Creating an internal connection
You must be a [Workspace Owner](https://www.notion.com/help/add-members-admins-guests-and-groups) to create a connection.
Navigate to the Developer portal.
In the **Build** section of the sidebar, select **Internal connections**.
Click **Create a new connection**, enter a connection name, and choose the workspace where the connection can be installed.
After creation, visit the **Configuration** tab to retrieve your **Installation access token**.
You can also configure the connection's [capabilities](/reference/capabilities) — such as whether it can read content, update content, insert content, or read user information — from the **Configuration** tab.
## Granting page access
Before your connection can access any data, it must be explicitly granted access to pages or databases. There are two ways to do this.
### From the Developer portal
The connection owner can manage access directly from the **Content access** tab in the Developer portal. This is the quickest way to get started after creating a connection.
Open your connection in the Developer portal.
Click the **Content access** tab.
Click **Edit access**, then select the pages and databases you want the connection to access.
### From the Notion UI
Workspace members can also share individual pages with the connection from within Notion.
Open a Notion page you want the connection to access.
Click the **•••** menu in the top-right corner of the page.
Select **Connections**, then click **+ Add connection**.
Search for your connection and select it.
Confirm the connection can access the page and all of its child pages.
**Your connection needs page access to make API requests**
A newly created connection has no page access by default. If you skip this step, any API request will return an error. Use the **Content access** tab or **Add connections** menu to grant access before making requests.
## Authentication
Internal connections authenticate every API request using the API token retrieved from the **Configuration** tab. Include the token in the `Authorization` header:
```http HTTP theme={null}
GET /v1/pages/b55c9c91-384d-452b-81db-d1ef79372b75 HTTP/1.1
Authorization: Bearer {INTEGRATION_TOKEN}
```
If you're using the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js), the token is set once when initializing the client:
```javascript JavaScript theme={null}
const { Client } = require("@notionhq/client")
const notion = new Client({
auth: process.env.NOTION_TOKEN,
})
```
**Keep your token secret.** Never store the token in source code or commit it to version control. Use environment variables or a secret manager instead. If your token is accidentally exposed, you can refresh it from the connection's **Configuration** tab.
[Learn more: Best practices for handling API keys](/guides/get-started/handling-api-keys)
For the full details on internal connection authentication, see the [Authorization guide](/guides/get-started/authorization#internal-connection-auth-flow-set-up).
## Next steps
Build your first connection with a hands-on tutorial.
Explore all available endpoints.
# List on the Marketplace
Source: https://developers.notion.com/guides/get-started/marketplace-listing
Learn how to list your public connection on the Notion Marketplace.
The [Notion Marketplace](https://www.notion.com/integrations/all) is how Notion users discover and connect public connections. Listing your connection there puts it in front of every Notion user — and it's a separate step from building the connection itself, so you can ship whenever you're ready.
This guide assumes you already have a public connection. If you haven't created one yet, see the [Public connections](/guides/get-started/public-connections) guide first.
This guide covers how to:
* Start a new Marketplace listing
* Submit your listing for review
* Understand the review process and timeline
Only public connections with an installation scope of **Any workspace** can be listed on the Marketplace. Internal connections and **Selected workspaces only** public connections are not eligible. Installation scope is set when the connection is created and can't be changed afterward — see [Installation scope](/guides/get-started/public-connections#installation-scope).
## Start a new listing
Navigate to the Marketplace listing dashboard.
In the **Listings** section of the sidebar, select **Connections**.
Under **Drafts**, click **Start a new connection listing**.
Fill in the listing details, including:
* Listing name and description
* Category and tags
* Listing images and logo
* The public connection to associate with this listing
Save your listing as a draft. You can return to edit it at any time before submitting.
## Submit for review
When your listing is ready, submit it for review from the **Connections** listing page.
From the **Listings > Connections** page, find your draft listing.
Review all listing details to ensure they are complete and accurate.
Submit your listing for review by the Notion team.
After submission, your listing moves to the **Submitted** section where you can track its review status.
## Review process
The Notion team reviews every listing submission. Track the status of your submission from the **Listings > Connections** page in the Marketplace listing dashboard. See the [FAQ](#frequently-asked-questions) below for review timelines.
If approved, the listing appears in the [Notion Marketplace](https://www.notion.com/integrations/all). If changes are required, the Notion team sends feedback and you can resubmit after making updates.
## Connection gallery best practices
For guidance on the review process and best practices to get your connection approved, check out the [Notion Connection Gallery Best Practices](https://www.notion.com/notiondevs/Notion-Integration-Gallery-Best-Practices-997825927fd6473e89617ce0c329145c?pvs=4) guide.
## Frequently asked questions
After submission, expect to hear back from our team within 5-10 business days via email. Check the status of your connection submission from the **Listings > Connections** page in the Marketplace listing dashboard.
The Notion team sends an email explaining why your listing was not approved. Check the status from the **Listings > Connections** page.
Connections are rejected for various reasons, from brand/trademark issues to quality concerns to situations where the baseline connection criteria isn't met. We encourage developers to review our feedback, make necessary changes, and resubmit. Our goal is to help you create high-quality and valuable tools for the Notion community.
No. Public connections work independently of Marketplace listings. Listing on the Marketplace is optional and helps your connection reach a wider audience, but your connection can be used via its OAuth flow without being listed.
# Overview
Source: https://developers.notion.com/guides/get-started/overview
Discover what Notion connections are, when to use each type, and what you can build.
## Using Notion API
Notion connections let you connect your workspace to external tools and automate workflows through code. With the REST API, you can read, create, and update nearly everything in a workspace — pages, databases, users, comments, and more.
When you create a connection, you define what it can do: which API endpoints it can call, what content it can read or write, and how it authenticates. Each connection gets its own credentials and its own set of permissions.
## What is a Notion connection?
A Notion [connection](https://www.notion.com/help/add-and-manage-connections-with-the-api) — sometimes called an integration — connects your workspace to external apps and tools. That could be a SaaS product, an automation script, or a custom tool you've built.
Connections are added to Notion workspaces and require **explicit permission** from users to access Notion pages and databases.
Notion already has a [library](https://www.notion.com/integrations/all) of connections you can browse. For developers who want to build their own, Notion supports internal connections, public connections, and personal access tokens — all powered by the same REST API.
## Connection types
Notion supports three authentication models:
* **Internal connections** are scoped to a single workspace and use a static API token. They're ideal for custom automations and workflows — things like syncing data, sending notifications, or building internal dashboards.
* **Public connections** use OAuth 2.0 for authentication. At creation time, you choose their [installation scope](/guides/get-started/public-connections#installation-scope): **Any workspace** (any Notion user can install; Marketplace-eligible) or **Selected workspaces only** (restricted to workspaces you select; not Marketplace-eligible).
* **Personal access tokens (PATs)** are user-scoped tokens for scripts, CLI workflows, Workers, and tools that should act as one Notion user. A PAT uses the creator's workspace membership and page permissions. See [Personal access tokens](/guides/get-started/personal-access-tokens).
Public connections must undergo a Notion security review before being [listed on the Marketplace](/guides/get-started/marketplace-listing). You can create and use a public connection without listing it.
### Comparison
| Feature | Internal connections | Public connections | Personal access tokens |
| :----------------- | :----------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- |
| Best for | Team-owned automations in one workspace. | Apps or services used by many Notion users or workspaces. | User-owned scripts, CLI workflows, Workers, and trusted tools. |
| Installation scope | Single workspace. | Any workspace, or a specific set of workspaces chosen at creation time. Scope can't change after creation. | One user in one workspace. |
| User access | Only members of the workspace where it's installed. | Any user in a workspace where the connection is allowed to install. | The member who created the token. |
| Content access | Granted directly to the connection, not tied to any specific user. | Users choose which pages to share during the OAuth flow or via the Add connections menu. | Uses the creator's Notion permissions; pages do not need to be shared with a bot. |
| Authentication | Static API token. | OAuth 2.0. | Static bearer token. |
**Looking for SCIM or SAML SSO?**
Enterprise identity management (user provisioning, group management, and Single Sign-On) is covered in Notion's Help Center, not in these API docs.
## Shared concepts
All connection and token types share a few core concepts.
### Capabilities
Every connection or token has a set of capabilities that control what it can do — read content, update content, insert content, read comments, and more. You configure capabilities when you create a connection or PAT. See the [Capabilities reference](/reference/capabilities) for the full list.
Create, update, and retrieve page content.
Manage database, properties, entries, and schemas.
Create and configure database views programmatically.
Manage data sources, properties, entries, and schemas.
Upload and attach files to pages and databases.
Handle page and inline comments.
Search through workspace content.
Access user profiles and permissions.
### Content access
Connections must have access to pages and databases before they can interact with them. The mechanism differs by type:
* **Internal connections** can be granted access in two ways: the connection owner can add pages directly from the **Content access** tab in the Developer portal, or workspace members can share pages via the **Add connections** menu in Notion.
* **Public connections** use the OAuth page picker, where users select which pages to grant access to during the authorization flow.
* **Personal access tokens** use the token creator's existing Notion permissions. If the creator can access a page in Notion, a PAT with the right capabilities can access it through the API.
See the [Internal connections](/guides/get-started/internal-connections), [Public connections](/guides/get-started/public-connections), and [Personal access tokens](/guides/get-started/personal-access-tokens) guides for specifics on how content access works for each type.
### Webhooks
Connections can subscribe to real-time events — like page updates, property changes, and new comments — via webhooks. This allows your connection to react to changes in Notion without polling the API. See the [Webhooks guide](/reference/webhooks) for details on setting up webhook subscriptions.
## Getting started
The fastest way to start building is the [**Quickstart**](/guides/get-started/quick-start) — create a personal access token, make your first API request, and see a new page appear in your workspace in under two minutes.
Once you're ready to build further, choose the authentication model that fits your use case:
[**Personal access tokens**](/guides/get-started/personal-access-tokens) — Authenticate as yourself for scripts, CLI workflows, Workers, or trusted tools.
[**Internal connections**](/guides/get-started/internal-connections) — Build team-owned automations with a dedicated bot identity scoped to one workspace.
[**Public connections**](/guides/get-started/public-connections) — Build apps for other Notion users with OAuth 2.0.
[**Authorization**](/guides/get-started/authorization) — Implement the OAuth 2.0 flow for public connections.
[**Handling API keys**](/guides/get-started/handling-api-keys) — Secure and manage your API tokens in production.
[**Preparing for users**](/guides/get-started/preparing-for-users) — Set up databases, pages, and views automatically when users install your connection.
[**List on the Marketplace**](/guides/get-started/marketplace-listing) — Make your public connection discoverable to all Notion users.
## Resources
Explore the links below to get started, and join the [Notion Devs Slack community](https://join.slack.com/t/notiondevs/shared_invite/zt-3u9oid9q8-HLUBmMVWYK~g9HFo4U4raA) to share your projects and connect with fellow developers.
# Personal access tokens
Source: https://developers.notion.com/guides/get-started/personal-access-tokens
Create and use personal access tokens for user-scoped API and Workers access.
Personal access tokens (PATs) let one Notion user call the Notion API or work with Notion Workers. A PAT belongs to one workspace. For Notion API requests, it uses the workspace membership and page permissions of the person who created it.
Use a PAT when API requests should act as you and you don't need an [internal connection](/guides/get-started/internal-connections) or the [OAuth flow for a public connection](/guides/get-started/public-connections).
## When to use a PAT
Use a PAT for personal or developer-owned workflows that one Notion user should own:
* Local scripts, notebooks, and command-line tools that automate work in your own workspace.
* Development and testing against the Notion API before you create a shared connection.
* Third-party tools that ask you to paste a Notion token and should act with your Notion permissions.
* [Notion Workers](/workers/get-started/overview) development and deployment with the Notion CLI.
Don't use a PAT to authenticate a product used by many Notion users. Create a [public connection](/guides/get-started/public-connections) so each user can authorize access with OAuth. For a team-owned automation that should not depend on one person's permissions, use an [internal connection](/guides/get-started/internal-connections).
## How PATs work
Create PATs in the Developer portal. For each PAT, you choose:
* A token name.
* The workspace the token belongs to.
* Capabilities for the token.
You can give a PAT either or both of these capabilities:
| Capability | What it allows |
| :------------- | :--------------------------------------------------------------------------------------------------------------------------------- |
| **Notion API** | Read, create, update, and search content; read and create comments; and read supported user information through Notion's REST API. |
| **Workers** | Deploy and manage [Notion Workers](/workers/get-started/overview) with the Notion CLI. |
The workspace's PAT creation policy controls who can use the Notion API capability. Workspace members can receive the Workers capability when Notion Workers is available to the workspace.
PATs authenticate requests the same way other Notion API tokens do:
```http HTTP theme={null}
GET /v1/users/me HTTP/1.1
Authorization: Bearer {PERSONAL_ACCESS_TOKEN}
Notion-Version: 2026-03-11
```
```javascript JavaScript theme={null}
// Save this example in an .mjs file.
import { Client } from "@notionhq/client"
const notion = new Client({
auth: process.env.NOTION_PAT,
})
```
## Permissions and content access
A PAT acts as the user who created it:
* It can access pages, data sources, databases, comments, files, and other resources that the creator can access.
* It does not need pages to be shared with a bot through **Add connections**.
* If the creator loses access to a page or leaves the workspace, the PAT loses that access too.
* API behavior that depends on an authenticated user, such as `"me"` filters or workspace-level private page creation, uses the PAT creator.
An internal connection operates as a separate bot user and can access only pages shared with that connection. A PAT uses a real user's permissions, so it is best for work owned by that user rather than team-owned automations.
[List all users](/reference/get-users) is not available to PATs. A PAT can use [Retrieve token's bot user](/reference/get-self) to retrieve the authorized user, and [Retrieve a user](/reference/get-user) can retrieve that same user.
## Workspace admin controls
Workspace admins can manage PATs from **Settings → Connections**:
* View all PATs created in the workspace, including active, expired, and revoked tokens.
* Search and filter tokens by name, creator, and status.
* See who created a token and, for revoked tokens, who revoked it.
* Revoke active or expired PATs.
* Configure who can create PATs with Notion API access.
Admins cannot view or copy another member's token value. The token creator can copy it only when they create the PAT; Notion does not show it again.
If an admin changes the workspace policy so a member is no longer allowed to create PATs with Notion API access, that member's existing PATs stop working for Notion API requests. Those requests return an `unauthorized` error until the policy allows the member again or the member uses a different valid token.
Organization owners can also use [List personal access tokens](/reference/admin/list-personal-access-tokens) and [Revoke a personal access token](/reference/admin/revoke-personal-access-token) to automate these tasks with the Admin API.
### Who can create PATs
[Guests and restricted members](https://www.notion.com/help/whos-who-in-a-workspace) cannot create PATs or log in with the Notion CLI (`ntn login`). Only full workspace members can create tokens, subject to the workspace's PAT creation policy below. Workspace owners can always create PATs with Notion API access.
| Plan | Default PAT creation policy | Admin controls |
| :--------- | :------------------------------------ | :------------------------------------------------------------------------------------------------------------------- |
| Free | Workspace owners only. | Not configurable. |
| Plus | All workspace members. | Not configurable. |
| Business | Workspace owners only. | Admins can choose **Workspace owners only** or **All workspace members**. |
| Enterprise | Workspace owners and selected groups. | Admins can choose **Workspace owners only**, **Workspace owners and selected groups**, or **All workspace members**. |
On Enterprise, admins manage selected groups in the PAT creator settings. If no groups are selected, only workspace owners can create PATs with Notion API access.
## Create a PAT
Open Personal access tokens in the Developer portal.
Select **New token**.
Name the token, then choose its capabilities and expiration. If a workspace picker appears, select the workspace the token belongs to.
Select **Create token**, then copy the token value and store it securely. You can't view it again.
Choose an **Expiration** of **7 days**, **30 days**, **90 days**, **180 days**, or **1 year**. The form shows the exact expiration date before and after you create the token. If you don't choose an expiration, the token expires after 1 year. Create a new PAT and update your scripts or tools before then. Expired tokens return an `unauthorized` error.
## Revoke a PAT
Revoke a PAT immediately if it is exposed, no longer needed, or associated with a tool you no longer trust.
* Token creators can revoke their own PATs from the Developer portal.
* Workspace admins can revoke any PAT in their workspace from **Settings → Connections → All personal access tokens**.
After revocation, the token immediately stops working for scripts, tools, Workers, and API requests that use it.
## Security best practices
Keep PATs as secure as passwords:
* Store PATs in environment variables or a secret manager.
* Do not commit PATs to source control.
* Use a separate PAT per script, tool, or environment so you can revoke one token without breaking unrelated workflows.
* Grant only the capabilities the workflow needs.
* Revoke tokens you no longer use.
For more guidance, see [Best practices for handling API keys](/guides/get-started/handling-api-keys).
# Preparing your connection for users
Source: https://developers.notion.com/guides/get-started/preparing-for-users
Learn how to create databases, pages, and views in a user's workspace right after they install your public connection.
Most public connections need specific databases, pages, or views to work. Traditionally, that meant waiting for the user to [share pages](/guides/get-started/authorization) manually or [duplicate a static template](/guides/get-started/authorization#prompt-for-a-connection-with-a-notion-template-option) during OAuth — both add friction and delay how quickly your connection delivers value.
With the Notion API, public connections can skip those steps entirely. Right after a user authorizes your connection, you can create the exact databases, pages, and views your connection needs — no extra user action required.
In this guide, you'll learn how to:
* Create databases and pages directly in a user's workspace
* Configure views with filters, sorts, and layout types
* Populate pages using database templates
## How it works
The user goes through the standard [OAuth flow](/guides/get-started/authorization). You receive an `access_token` with the capabilities your connection requested. No template URL is needed.
Use the API to create [databases](/reference/create-database) and [pages](/reference/post-page) at the workspace level. These appear in the user's **Private** section in Notion.
Use the [views API](/guides/data-apis/working-with-views) to add views (table, board, calendar, etc.) to your newly created databases, with the filters and sorts your connection needs.
If your databases have [data source templates](/guides/data-apis/creating-pages-from-templates), you can create pages that start from those templates for a richer initial experience.
## Creating workspace-level content
Public connections and [personal access tokens](/guides/get-started/personal-access-tokens) can create pages and databases at the **workspace level** by omitting the `parent` parameter (or setting it to `{ "type": "workspace", "workspace": true }`). This places the content in the associated user's Private section.
This capability is only available to **public connections**. Internal connections cannot create workspace-level content because they aren't owned by a single user.
### Create a database
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/databases \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"title": [{ "type": "text", "text": { "content": "Project Tracker" } }],
"is_inline": false,
"initial_data_source": {
"properties": {
"Task": { "title": {} },
"Status": {
"status": {
"options": [
{ "name": "Not started", "color": "default" },
{ "name": "In progress", "color": "blue" },
{ "name": "Done", "color": "green" }
]
}
},
"Assignee": { "people": {} },
"Due date": { "date": {} }
}
}
}'
```
```javascript JavaScript expandable theme={null}
const { Client } = require("@notionhq/client");
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const database = await notion.databases.create({
// Omitting "parent" creates a workspace-level database
title: [{ type: "text", text: { content: "Project Tracker" } }],
is_inline: false,
initial_data_source: {
properties: {
Task: { title: {} },
Status: {
status: {
options: [
{ name: "Not started", color: "default" },
{ name: "In progress", color: "blue" },
{ name: "Done", color: "green" },
],
},
},
Assignee: { people: {} },
"Due date": { date: {} },
},
},
});
const dataSourceId = database.data_sources[0].id;
```
The new database is created with one data source and one default Table view. Store the `database.id` and `database.data_sources[0].id` — you'll need them to create views and pages.
### Create a standalone page
```bash cURL expandable theme={null}
curl -X POST https://api.notion.com/v1/pages \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"properties": {
"title": {
"title": [{ "type": "text", "text": { "content": "Getting Started" } }]
}
},
"children": [
{
"object": "block",
"type": "heading_2",
"heading_2": {
"rich_text": [{ "type": "text", "text": { "content": "Welcome!" } }]
}
},
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{ "type": "text", "text": { "content": "This page was created by your connection. You can move it anywhere in your workspace." } }
]
}
}
]
}'
```
```javascript JavaScript expandable theme={null}
const page = await notion.pages.create({
// Omitting "parent" creates a workspace-level page
properties: {
title: {
title: [{ type: "text", text: { content: "Getting Started" } }],
},
},
children: [
{
object: "block",
type: "heading_2",
heading_2: {
rich_text: [{ type: "text", text: { content: "Welcome!" } }],
},
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
type: "text",
text: {
content:
"This page was created by your connection. You can move it anywhere in your workspace.",
},
},
],
},
},
],
});
```
## Adding views
After creating a database, you can add views that match your connection's use cases. Each database starts with a default Table view, but you'll likely want to create additional views with specific filters, sorts, and layout types.
For a project tracker, you might want a Board view grouped by status and a Calendar view for due dates:
```bash cURL expandable theme={null}
# Board view: tasks grouped by status
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"database_id": "DATABASE_ID",
"data_source_id": "DATA_SOURCE_ID",
"name": "Task board",
"type": "board"
}'
# Calendar view: tasks by due date
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"database_id": "DATABASE_ID",
"data_source_id": "DATA_SOURCE_ID",
"name": "Schedule",
"type": "calendar"
}'
# Filtered table: only active tasks
curl -X POST https://api.notion.com/v1/views \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"database_id": "DATABASE_ID",
"data_source_id": "DATA_SOURCE_ID",
"name": "Active tasks",
"type": "table",
"filter": {
"property": "Status",
"status": {
"does_not_equal": "Done"
}
},
"sorts": [
{
"property": "Due date",
"direction": "ascending"
}
]
}'
```
```javascript JavaScript expandable theme={null}
// Board view: tasks grouped by status
const boardView = await notion.views.create({
database_id: database.id,
data_source_id: dataSourceId,
name: "Task board",
type: "board",
});
// Calendar view: tasks by due date
const calendarView = await notion.views.create({
database_id: database.id,
data_source_id: dataSourceId,
name: "Schedule",
type: "calendar",
});
// Filtered table: only active tasks
const activeView = await notion.views.create({
database_id: database.id,
data_source_id: dataSourceId,
name: "Active tasks",
type: "table",
filter: {
property: "Status",
status: {
does_not_equal: "Done",
},
},
sorts: [
{
property: "Due date",
direction: "ascending",
},
],
});
```
See the [Working with views](/guides/data-apis/working-with-views) guide for full details on creating, updating, and querying views.
## Applying templates
If your connection pre-configures [database templates](https://www.notion.com/help/database-templates) for the data source, you can create pages that start from those templates. This is useful for providing users with structured starting points — for example, a "Bug report" template with pre-filled sections.
```bash cURL expandable theme={null}
# List available templates for the data source
curl -X GET "https://api.notion.com/v1/data_sources/DATA_SOURCE_ID/templates" \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
# Create a page using the default template
curl -X POST https://api.notion.com/v1/pages \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"parent": {
"type": "data_source_id",
"data_source_id": "DATA_SOURCE_ID"
},
"properties": {
"Task": {
"title": [{ "type": "text", "text": { "content": "My first task" } }]
}
},
"template": {
"type": "default"
}
}'
```
```javascript JavaScript expandable theme={null}
// List available templates for the data source
const templates = await notion.dataSources.listTemplates({
data_source_id: dataSourceId,
});
// Create a page using the default template
const page = await notion.pages.create({
parent: {
type: "data_source_id",
data_source_id: dataSourceId,
},
properties: {
Task: {
title: [{ type: "text", text: { content: "My first task" } }],
},
},
template: {
type: "default",
},
});
```
Template content is applied asynchronously after the page is created. If your connection needs to take action once the template is fully applied, use [webhooks](/reference/webhooks) to listen for `page.content_updated` events. See the [Creating pages from templates](/guides/data-apis/creating-pages-from-templates) guide for the full workflow.
## Programmatic setup vs. template duplication
| | Template URL (OAuth) | Programmatic setup |
| :--------------------- | :------------------------------------------------- | :------------------------------------------------------ |
| **Customization** | Static — every user gets the same template | Dynamic — tailor content to each user |
| **Schema control** | Snapshot; changes require updating the source page | Full control over properties and views at creation time |
| **Multiple databases** | One template page per connection | Create as many databases and pages as needed |
| **View configuration** | Views duplicated as-is | Create views with specific filters, sorts, and types |
| **User interaction** | User must choose "Duplicate template" during OAuth | No extra steps — setup happens after authorization |
Template duplication still works well for simple connections where a single static page is enough. Use programmatic setup when you need multiple resources, per-user customization, or want to keep the workspace in sync with an external system.
**What's next**
Now that you know how to set up workspace content, explore the APIs used in this guide:
* [Working with databases](/guides/data-apis/working-with-databases) — schemas, querying, and page management
* [Working with views](/guides/data-apis/working-with-views) — creating, updating, and querying views
* [Creating pages from templates](/guides/data-apis/creating-pages-from-templates) — using data source templates
* [Authorization](/guides/get-started/authorization) — the OAuth flow for public connections
# Public connections
Source: https://developers.notion.com/guides/get-started/public-connections
Learn how public connections work, how users authorize them, and how to create one.
## What is a public connection?
A public connection is an OAuth connection that users install into their Notion workspaces. Unlike [internal connections](/guides/get-started/internal-connections), which are scoped to a single workspace with a static token, public connections follow the [OAuth 2.0](https://oauth.net/2/) protocol: each user who authorizes the connection receives their own access token, scoped to their workspace.
When you create a public connection, you also choose its [installation scope](#installation-scope) — either **Any workspace** (Marketplace-eligible) or **Selected workspaces only** (not Marketplace-eligible).
If you only need a token for your own scripts, CLI workflows, Workers, or trusted tools, use a [personal access token](/guides/get-started/personal-access-tokens). PATs also act as one Notion user, but they do not provide an OAuth install flow for other users.
This guide covers:
* How public connections differ from internal connections
* How installation scope controls who can install your connection
* How users authorize a public connection via OAuth
* How to create a public connection in the Developer portal
## How public connections differ from internal connections
The key differences come down to scope, identity, and how access is granted:
* **Scope:** Internal connections work in one workspace; public connections can install into many. [Installation scope](#installation-scope) controls which workspaces are eligible.
* **Identity:** Internal connections operate as their own bot user with permissions independent of any specific person. Public connections act on behalf of the individual user who authorized them — the access token is tied to that user. [Personal access tokens](/guides/get-started/personal-access-tokens) are also tied to one user, but they are created directly by that user instead of through OAuth.
* **Page access:** Internal connections require workspace members to manually share pages via the "Add connections" menu. Public connections use the OAuth page picker, where users choose which pages to grant access to during the authorization flow.
For a full comparison, see the [comparison table](/guides/get-started/overview#comparison) in the Overview.
## Installation scope
Every public connection has an **installation scope** that controls which workspaces can install it. You pick the scope when you create the connection.
| Scope | Who can install | Marketplace eligible |
| :----------------------- | :----------------------------------------------- | :------------------- |
| Any workspace | Any Notion user, in any workspace. | Yes |
| Selected workspaces only | Only the workspaces you select at creation time. | No |
Installation scope is set once, at creation time, and can't be changed afterward. If you pick **Selected workspaces only** and later want to list on the Marketplace, create a new connection.
## How users authorize a public connection
When a user wants to use your public connection, they go through an OAuth authorization flow:
The user visits the connection's authorization URL. Find this URL in the **Configuration** tab of your connection in the Developer portal.
Notion presents a prompt describing the connection's [capabilities](/reference/capabilities) — what it will be able to do in the user's workspace.
The user selects which pages to grant the connection access to using the page picker.
After the user approves, Notion redirects them to your redirect URI with a temporary authorization code.
Your connection exchanges the code for an access token, which is used for all subsequent API requests on behalf of that user.
Public connections can also offer a Notion template during the auth flow. If configured, users can choose to duplicate the template into their workspace instead of selecting existing pages. See the [Authorization guide](/guides/get-started/authorization#prompt-for-a-connection-with-a-notion-template-option) for details on configuring templates.
After a user authorizes a public connection, only that user can interact with the connection in their workspace. If multiple members in a workspace want to use the same public connection, each user needs to complete the authorization flow individually.
## Creating a public connection
Navigate to the Developer portal.
In the **Build** section of the sidebar, select **Public connections**.
Click **Create new connection** and fill in the required fields, including:
* Connection name and development workspace
* [Redirect URI(s)](https://www.oauth.com/oauth2-servers/redirect-uris/) for the OAuth flow
* [Installation scope](#installation-scope) — choose **Any workspace** or **Selected workspaces only** (if you pick the latter, select the workspaces from the list that appears)
* Connection [capabilities](/reference/capabilities) (read content, update content, insert content, etc.)
After creation, visit the **Configuration** tab to retrieve your **OAuth client ID** and **OAuth client secret**. The client ID identifies your connection to Notion during the OAuth flow, and the client secret proves your connection is who it claims to be. Both are required when your server exchanges the authorization code for an access token. See the [Authorization guide](/guides/get-started/authorization) for the full implementation.
Marketplace listing details (such as descriptions, categories, and images) are managed separately through the **Listings** section. See [List on the Marketplace](/guides/get-started/marketplace-listing) for details.
## Next steps
Implement the full OAuth 2.0 flow for your public connection.
Automate user onboarding after they install your connection.
# API quickstart
Source: https://developers.notion.com/guides/get-started/quick-start
Create a page in your Notion workspace with a personal access token and a single API request.
You need a Notion account and a terminal.
## Step 1: Get a personal access token
A personal access token (PAT) lets you authenticate API requests as yourself without setting up a connection or completing an OAuth flow.
Open Personal access tokens in the Developer portal.
Select **New token**.
Enter a name, select the **Notion API** capability, and then select **Create token**. If a workspace picker appears, select a workspace first.
Copy the token and save it somewhere secure. You won't be able to see it again.
**Don't see the option to create a token?**
On Business and Enterprise plans, PAT creation is restricted by default. Ask a workspace owner to enable it in **Settings → Connections**.
See [Who can create PATs](/guides/get-started/personal-access-tokens#who-can-create-pats).
Set the token as an environment variable so you can use it in the examples below. This lasts for your current terminal session — run it again if you open a new window.
```bash macOS / Linux theme={null}
export NOTION_API_KEY=ntn_***
```
```powershell Windows (PowerShell) theme={null}
$env:NOTION_API_KEY = "ntn_***"
```
## Step 2: Create a page
Make a POST request to the [Create a page](/reference/post-page) endpoint with markdown content. The API creates a private page in your workspace, using the `# heading` as the page title automatically.
```bash curl theme={null}
curl -X POST https://api.notion.com/v1/pages \
-H "Authorization: Bearer $NOTION_API_KEY" \
-H "Notion-Version: 2026-03-11" \
-H "Content-Type: application/json" \
-d '{
"icon": { "emoji": "🚀" },
"markdown": "# Hello from the API\n\n## Welcome\n\nThis page was created with the Notion API. You just made your first request!\n\n- Read the [API reference](https://developers.notion.com/reference/intro)\n- Explore [examples](https://developers.notion.com/page/examples)"
}'
```
```http HTTP theme={null}
POST https://api.notion.com/v1/pages
Authorization: Bearer {PERSONAL_ACCESS_TOKEN}
Content-Type: application/json
Notion-Version: 2026-03-11
{
"icon": { "emoji": "🚀" },
"markdown": "# Hello from the API\n\n## Welcome\n\nThis page was created with the Notion API. You just made your first request!\n\n- Read the [API reference](https://developers.notion.com/reference/intro)\n- Explore [examples](https://developers.notion.com/page/examples)"
}
```
```javascript JavaScript theme={null}
// Save as quickstart.mjs after running: npm install @notionhq/client
import { Client } from "@notionhq/client";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
async function main() {
const page = await notion.pages.create({
icon: { emoji: "🚀" },
markdown: [
"# Hello from the API",
"",
"## Welcome",
"",
"This page was created with the Notion API. You just made your first request!",
"",
"- Read the [API reference](https://developers.notion.com/reference/intro)",
"- Explore [examples](https://developers.notion.com/page/examples)",
].join("\n"),
});
console.log("Created page:", page.url);
}
main();
```
The `markdown` field accepts [Notion-flavored Markdown](/guides/data-apis/enhanced-markdown) — headings, lists, code blocks, links, and more. The API converts it to Notion blocks for you.
Notion pages are made up of **blocks** — headings, paragraphs, lists, and more. When you send `markdown`, the API converts it into this block structure automatically.
You can also build this structure directly using the `children` field. Here's what the same page looks like expressed as blocks:
```json theme={null}
{
"icon": { "emoji": "🚀" },
"properties": {
"title": [{ "text": { "content": "Hello from the API" } }]
},
"children": [
{
"object": "block",
"type": "heading_2",
"heading_2": {
"rich_text": [{ "text": { "content": "Welcome" } }]
}
},
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"text": {
"content": "This page was created with the Notion API. You just made your first request!"
}
}
]
}
}
]
}
```
The block model gives you precise control over every element — formatting, colors, toggles, and block types that markdown can't express. Use `markdown` when you want simplicity, and `children` when you need that control.
See [Working with page content](/guides/data-apis/working-with-page-content) to learn more about the block model.
## Check the result
A successful response returns a page object:
```json Response theme={null}
{
"object": "page",
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"created_time": "2025-01-15T09:30:00.000Z",
"last_edited_time": "2025-01-15T09:30:00.000Z",
"icon": {
"type": "emoji",
"emoji": "🚀"
},
"parent": {
"type": "workspace",
"workspace": true
},
"properties": {
"title": {
"id": "title",
"type": "title",
"title": [{ "plain_text": "Hello from the API" }]
}
},
"url": "https://www.notion.com/Hello-from-the-API-a1b2c3d4e5f67890abcdef1234567890",
"public_url": null
}
```
To see your new page:
* **From the response:** Copy the `url` value and open it in your browser.
* **From Notion:** Look in **Private** in your sidebar for 🚀 **Hello from the API**.
Open the page and you should see your heading, paragraph, and bullet list inside.
Error responses return a JSON object with a `code` and `message`:
```json theme={null}
{
"object": "error",
"status": 401,
"code": "unauthorized",
"message": "API token is invalid."
}
```
| Error | Fix |
| :----------------- | :---------------------------------------------------------------------------------- |
| `unauthorized` | Double-check that your token is correct and hasn't expired. |
| `validation_error` | Check the request body against the [Create a page](/reference/post-page) reference. |
For the full list of error codes, see [Status codes](/reference/status-codes).
## Next steps
Now that you've made your first request, explore what else you can build — create databases, query content, manage comments, upload files, and more.
Browse every endpoint, request parameter, and response field.
Official client library for Node.js.
# FAQs
Source: https://developers.notion.com/guides/get-started/upgrade-faqs-2025-09-03
Commonly asked questions about migrating to 2025-09-03.
In September 2025, Notion is launching several features to improve what you can do with databases. This includes support for multiple **data sources** under a single **database**, each of which can have a different set of properties (schemas). The **database** becomes a *container* for one or more **data sources**.
To learn more about data sources in the Notion app and related features, visit our [help center page](https://www.notion.com/help/data-sources-and-linked-databases).
Prior to this release, **databases** were limited to one **data source**, so the **data source ID** was hidden. Now that multiple data sources are supported, we need a way to identify the specific data source for a request. Starting from the `2025-09-03` API version, Notion is providing a new set of APIs under `/v1/data_sources` for managing each **data source**. Most of your connection's existing database operations should move to this set of APIs.
The `/v1/databases` family of endpoints now refers to the **database** (container) as of `2025-09-03`. To discover the data sources available for a database, the database object includes a `data_sources` array, each having an `id` and a `name`. The data source ID can then by used with the `/v1/data_sources` APIs.
The concept of a database ID in the Notion app stays the same, and continues to be shown in the URL for a database followed by the ID of the specific view you're looking at. For example, in a link like `https://notion.so/workspace/248104cd477e80fdb757e945d38000bd?v=148104cd477e80bb928f000ce197ddf2`:
* `248104cd-477e-80fd-b757-e945d38000bd` is the **database** (container) ID.
* `148104cd477e80bb928f000ce197ddf2` is the database view (managing views is not currently supported in the API).
**Note**:
The ID of the specific **data source** you're looking at isn't embedded in the URL, but will be listed in a separate dropdown menu.
Here's a diagram of a scenario where a workspace has a top-level page that has a database with two data sources:
Going from top to bottom, here's a simplified run-through of how the API objects connect to one another:
* **Parent Page**:
* `parent` is `{"type": "workspace", "workspace": "true"}`
* No changes to how the page's Block children work.
* **Database**:
* `parent` is `{"type": "page_id", "page_id": ""}`
* `data_sources` is `[{"id": "...", "name": "Data Source"}, {"id": "...", "name": "Data Source"}]`
* **Data Source**:
* `parent` is `{"type": "database_id", "database_id": ""}`
* `database_parent` is `{"type": "page_id", "page_id": ""}`
* **Page**:
* `parent` is `{"type": "data_source_id", "data_source_id": ""}`
* No changes to how the page's Block children work.
User and bot permissions are managed at the **database** level, not per data source. This means that the level of access a Notion user or connection has (or doesn't have) is the same across all data sources in a database.
Unlike other databases, wikis won't support multiple data sources as part of the September 2025 launch. For this reason, and due to limited support in Notion's API, we recommend using alternative ways to structure your knowledge in Notion that don't involve wikis.
However, for completeness, here's a diagram of how parent/child relationships work in an example wiki scenario:
* Each family of APIs is summarized in the table below.
* Ones that are affected are marked in **bold** in the first column, and the `2025-09-03` changes are outlined in the second column.
* Ones that aren't affected are listed as "None" (some of which have explanatory comments as to why they aren't affected.)
| Endpoints | Changes |
| :--------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Authentication | None |
| Blocks | None |
| **Pages** | `parent` is a `data_source_id` instead of a `database_id` |
| **Databases** | Modified to act on the entire database (container) instead of its data sources via Create, Retrieve, or Update; see migration guide details above
Creating a database and its initial data source works the same way, but `properties` must be nested under `initial_data_source` as of `2025-09-03` |
| **Data Sources** | New set of APIs for operating on individual data sources under a database via Create, Update, Query, or Retrieve |
| Comments | None (comments can only have blocks or pages as parents, not databases or data sources, so they aren't affected) |
| File Uploads | None |
| **Search** | Filter value parameter refers to `"data_source"` instead of `"database"`; response results include each `"data_source"` object instead of `"database"` objects |
| Users | None |
The API version is already available to use for Notion API requests as of late August. We recommend starting the upgrade process detailed above at your earliest convenience if your connection is affected by the changes.
If your workspace is connected to any public connections (rather than an internal bot owned by you or your business), they may not have upgraded yet. If you rely on important workflows or automations, contact the third-party for any questions or issues regarding their timeline & support for databases with multiple sources.
### Notes on API versions
As a reminder, [API versioning](/reference/versioning) is determined by providing a mandatory `Notion-Version` HTTP header with each API request. If you're using the [TypeScript SDK](https://github.com/makenotion/notion-sdk-js), you might be configuring the version in one place where the Notion client is instantiated, or passing it explicitly for each request. You can follow the rest of this guide incrementally, upgrading each use of the API at a time at your convenience.
We're also extending the concept of API versioning to [connection webhooks](/reference/webhooks) to allow Notion to introduce backwards-incompatible changes without affecting your endpoint until you upgrade the API version in the connection settings. Ensure your webhook URL can handle events of both the old and new shape for a short period of time before making the upgrade.
We don't currently have any process for halting support of old Notion API versions. If we introduce a "minimum versioning" program in the future, we'll communicate this with all affected users with ample notice period (e.g. 6 months) and start with versions that came before `2022-06-28`.
However, even though API connections continue to work, we recommend upgrading to `2025-09-03` as soon as possible. That way, your system is ready for in-app creation of data sources, gains new functionality when working with databases, and you can help Notion's support teams better handle any questions or requests you have by making sure you're up-to-date.
### Behavior for existing connections
Connections using the `2022-06-28` API version (or older) will **continue to work with existing databases in Notion that have a single data source**. Webhooks will also generally continue to be delivered without any changes to the format.
However, if any Notion users create a second data source for a database in a workspace that's connected to your connection (starting on September 3, 2025), your database IDs are no longer precise enough for Notion to process the request.
Until you follow this guide to upgrade, Notion responds to requests involving a database ID with multiple data sources with validation errors that look like:
```json JSON Response theme={null}
{
"code": "validation_error",
"status": 400,
"message": "Databases with multiple data sources are not supported in this API version.",
"object": "error",
"additional_data": {
"error_type": "multiple_data_sources_for_database",
"database_id": "27a5d30a-1728-4a1e-a788-71341f22fb97",
"child_data_source_ids": [
"164b19c5-58e5-4a47-a3a9-c905d9519c65",
"25c104cd-477e-8047-836b-000b4aa4bc94"
],
"minimum_api_version": "2025-09-03"
}
}
```
The `additional_data` in the response can help you identify the relevant data source IDs to use instead, as you upgrade your connection.
We aim to improve functionality in our API through backwards-compatible features first and foremost. We've shipped several changes since 2022, including the [File Upload](/reference/file-upload) API, but generally aim to avoid having large sets of users have to go through a detailed upgrade progress when possible.
With these new changes to the Notion app, we want our connection partners, developer community, ambassadors, champions, and everyone else making great tools to unlock the power of multiple-source database containers. This involves rethinking what a "database ID" in the API can do and repurposing API endpoints, necessitating the `2025-09-03` version release.
## What’s Next
Upgrade your connections, learn more about connection webhooks, and stay tuned for any future updates to Notion's API:
# Upgrade guide
Source: https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03
Learn how to upgrade your connections to 2025-09-03.
We’ve released **Notion API version `2025‑09‑03`**, introducing first-class support for multi-source databases. This enables a single database to contain multiple linked data sources — unlocking powerful new workflows.
For more information about data sources, see [our FAQs.](/guides/get-started/upgrade-faqs-2025-09-03)
**However, this change is not backwards-compatible.** Most existing database connections must be updated to prevent disruptions.
**Code changes required**
If your connection is still using a previous API version and a user adds another data source to a database, **the following API actions will fail:**
* Create page when using the database as the parent
* Database read, write, or query
* Writing relation properties that point to that database
## What’s changing
* Most API operations that used `database_id` now require a `data_source_id`
* Several database endpoints have moved or been restructured to support the new data model
## What this guide covers
* A breakdown of what’s new and why it changed
* A step-by-step migration checklist to safely update your connections
## Upgrade checklist
Use this checklist to see exactly what must change before you bump Notion-Version to `2025-09-03`.
### Required steps across all of your connections
Add a discovery step to fetch and store the `data_source_id` to use in subsequent API calls.
Start sending `data_source_id` when creating pages or defining relations
Migrate database endpoints to data sources.
If you use the Search API, update result handling to process data source objects and possible multiple results per database
If using the TypeScript SDK, upgrade to the correct version and set the new version in your client
If using webhooks, handle the new shape and bump your subscription version
**Developer action required**
These steps primarily require code changes in your repositories or low-code platform. They cannot be fully completed through the Notion connection management UI.
## Step-by-step guide
### Step 1: Add a discovery step to fetch and store the `data_source_id`
First, identify the parts of your system that process database IDs. These may include:
* Responses of list and search APIs, e.g. [Search](/reference/post-search).
* Database IDs provided directly by users of your system, or hard-coded based on URLs in the Notion app.
* Events for connection webhooks (covered in the **Webhook changes** section below).
For each entry point that uses database IDs, start your migration process by introducing an API call to the new **Get Database API** (`GET /v1/databases/:database_id`) endpoint to retrieve a list of child `data_sources`. For this new call, make sure to use the `2025-09-03` version in the `Notion-Version` header, even if the rest of your API calls haven't been updated yet.
```json Get Database (JSON) expandable theme={null}
// GET /v1/databases/{database_id}
// Notion-Version: "2025-09-03"
// --- RETURNS -->
{
"object": "database",
"id": "{database_id}",
"title": [/* ... */],
"parent": {
"type": "page_id",
"page_id": "255104cd-477e-808c-b279-d39ab803a7d2"
},
"is_inline": false,
"in_trash": false,
"created_time": "2025-08-07T10:11:07.504-07:00",
"last_edited_time": "2025-08-10T15:53:11.386-07:00",
"data_sources": [
{
"id": "{data_source_id}",
"name": "My Task Tracker"
}
],
"icon": null,
"cover": null,
// ...
}
```
```typescript Get Database (JS SDK) expandable theme={null}
let notion = new Client({
auth: "{ACCESS_TOKEN}",
notionVersion: "2025-09-03",
})
const DATABASE_ID = "/* ... */"
try {
const response = await notion.request({
method: "get",
path: `databases/${DATABASE_ID}`,
})
const dataSources = response.data_sources
// [{ id: "...", name: "..." }, ...]
console.log(dataSources)
// In the existing, single-source database case, there will only
// be one data source.
const dataSource = dataSources[0]
} catch (error) {
// Handle `APIResponseError`
console.error(error)
}
// ... Remaining code, not migrated yet.
notion = new Client({
auth: "{ACCESS_TOKEN}",
notionVersion: "2022-06-28",
})
// ...
```
To get a data source ID in the Notion app, the settings menu for a database includes a "Copy data source ID" button under "Manage data sources":
Having access to the data source ID (or rather, *IDs*, once Notion users start adding 2nd sources for their existing databases) for a database lets you continue onto the next few steps.
### Step 2: Provide data source IDs when creating pages or relations
Some APIs that accept `database_id` in the body parameters now support providing a specific `data_source_id` instead. This works for any API version, meaning you can switch over at your convenience, before or after upgrading these API requests to use `2025-09-03`:
* Creating a page with a database (now: data source) parent
* Defining a database relation property that points to another database (now: data source)
#### Create page
In the [Create a page](/reference/post-page) API, look for calls that look like this:
```json Create Page (JSON) theme={null}
// POST /v1/pages
{
"parent": {
"type": "database_id",
"database_id": "..."
}
}
```
```typescript Create Page (TS SDK) theme={null}
const response = await notion.pages.create({
parent: {
type: "database_id",
database_id: DATABASE_ID,
}
})
```
Change these to use `data_source_id` parents instead, using the code from Step 1 to get the ID of a database's data source:
```json Create Page (JSON) theme={null}
// POST /v1/pages
{
"parent": {
"type": "data_source_id",
"data_source_id": "..."
}
}
```
```typescript Create Page (TS SDK) theme={null}
// Get dataSource from Step 1
const response = await notion.request({
method: "post",
path: "pages",
body: {
parent: {
type: "data_source_id",
data_source_id: dataSource.id,
},
}
})
```
#### Create or update database
For [database relation properties](/reference/property-object#relation), the API will include both a `database_id` and `data_source_id` fields in the read path instead of just a `database_id`.
In the write path, switch your connection to only provide the `data_source_id` in request objects.
```json Relation property response example theme={null}
"Projects": {
"id": "~pex",
"name": "Projects",
"type": "relation",
"relation": {
"database_id": "6c4240a9-a3ce-413e-9fd0-8a51a4d0a49b",
"data_source_id": "a42a62ed-9b51-4b98-9dea-ea6d091bc508",
"dual_property": {
"synced_property_name": "Tasks",
"synced_property_id": "JU]K"
}
}
}
```
Note that [database mentions](/reference/rich-text#database-mention-type-object) in rich text will continue to reference the database, not the data source.
### Step 3: Migrate database endpoints to data sources
The next step is to migrate each existing use of database APIs to their new data source equivalents, taking into account the differences between the old `/v1/databases` APIs and new `/v1/data_sources` APIs:
* Return very similar responses, but with `object: "data_source"`, starting from `2025-09-03`
* Accept a specific **data source ID** in query, body, and path parameters, not a database ID
* Exist under the `/v1/data_sources` namespace, starting from version `2025-09-03`
* Require a custom API request with `notion.request` if you're using the TypeScript SDK, since we won't upgrade to SDK v5 until you get to Step 4 (below).
The following APIs are affected. Each of them is covered by a sub-section below, with more specific Before vs. After explanations and code snippets:
#### Retrieve database
**Before (2022-06-28):**
* Retrieving a database with multiple data sources fails with a `validation_error` message.
* **For relation properties**: across *all* API versions, *both* the `database_id` and `data_source_id` are now included in the response object.
```json Retrieve Database (JSON) theme={null}
// GET /v1/databases/:database_id
{
// ...
}
```
```typescript Query Database (TS SDK) theme={null}
const response = await notion.databases.retrieve({
database_id: "...",
// ...
})
```
**After (2025-09-03):**
* The Retrieve Database API is now repurposed to return a list of `data_sources` (each with an `id` and `name`, as described in Step 1).
* The Retrieve *Data Source* API is the new home for getting up-to-date information on the properties (schema) of each data source under a database.
* The `object` field is always `"data_source"` and the `id` is specific to the data source.
* The `parent` object now identifies the `database_id` immediate parent of the data source.
* The database's parent (i.e. the data source's grandparent) is included as a separate field, `database_parent`, on the data source response.
* You can't use a database ID with the retrieve data source API, or vice-versa. The two types of IDs are not interchangeable.
```json Retrieve Data Source (JSON) expandable theme={null}
// Get `data_source_id` from Step 1
//
// GET /v1/data_sources/:data_source_id
{
"object": "data_source",
"id": "bc1211ca-e3f1-4939-ae34-5260b16f627c",
"created_time": "2021-07-08T23:50:00.000Z",
"last_edited_time": "2021-07-08T23:50:00.000Z",
"properties": {
"In stock": {
"id": "fk%5EY",
"name": "In stock",
"type": "checkbox",
"checkbox": {}
},
"Name": {
"id": "title",
"name": "Name",
"type": "title",
"title": {}
}
},
"parent": {
"type": "database_id",
"database_id": "6ee911d9-189c-4844-93e8-260c1438b6e4"
},
"database_parent": {
"type": "page_id",
"page_id": "98ad959b-2b6a-4774-80ee-00246fb0ea9b"
},
// ... (other properties omitted)
}
```
```typescript Retrieve Data Source (TS SDK) expandable theme={null}
// Get dataSource from Step 1
const response = await notion.request({
method: "get",
path: `data_sources/${dataSource.id}`,
// ...
})
// After upgrading TS SDK:
const response = await notion.dataSources.retrieve({
data_source_id: dataSource.id,
})
```
#### Query databases
**Before (2022-06-28):**
```json Query Database (JSON) theme={null}
// PATCH /v1/databases/:database_id/query
{
// ...
}
```
```typescript Query Database (TS SDK) theme={null}
const response = await notion.databases.query({
database_id: "...",
// ...
})
```
**After (2025-09-03):**
When you update the API version, the path of this API changes, and now accepts a data source ID. With the TS SDK, you'll have to switch this to temporarily use a custom `notion.request(...)`, until you upgrade to the next major version as part of Step 4.
```json Query Data Source (JSON) theme={null}
// PATCH /v1/data_sources/:data_source_id/query
{
// ...
}
```
```typescript Query Data Source (TS SDK) theme={null}
// Get dataSource from Step 1
const response = await notion.request({
method: "post",
path: `data_sources/${dataSource.id}/query`,
// ...
})
// After upgrading TS SDK:
const response = await notion.dataSources.query({
data_source_id: dataSource.id,
// ...
})
```
#### Create database
**Before (2022-06-28):**
* In `2022-06-28`, the Create Database API created a database and data source, along with its initial default view.
* **For relation properties**: across *all* API versions, *both* the `database_id` and `data_source_id` are now included in the response object.
* When providing relation properties in a request, you can either use `database_id`, `data_source_id`, or both, prior to making the API version upgrade.
* We recommend starting by switching your connection over to passing only a `data_source_id` for relation objects even in `2022-06-28` to precisely identify the data source to use for the relation and be ready for the `2025-09-03` behavior.
```json Create Database (JSON) theme={null}
// POST /v1/databases
{
"parent": {"type": "page_id", "page_id": "..."},
"properties": {...},
// ...
}
```
```typescript Create Database (TS SDK) theme={null}
const response = await notion.databases.create({
parent: {type: "page_id", page_id: "..."},
properties: {...},
// ...
})
```
**After (2025-09-03):**
* Continue to use the Create Database API even after upgrading, when you want to create both a database and its initial data source.
* `properties` for the initial data source you're creating now go under `initial_data_source[properties]` to better separate data source specific properties vs. ones that apply to the entire database.
* Other parameters apply to the database and continue to be specified at the top-level when creating a database (`icon`, `cover`, `title`).
* Only use the new Create Data Source API to add an additional data source (with a new set of `properties`) to an existing database.
* **For relation properties**: You can no longer provide a `database_id`. Notion continues to include both the `database_id` and `data_source_id` in the *response* for convenience, but the *request* object must **only contain `data_source_id`**.
```typescript Create Database with initial data source (JSON) theme={null}
// POST /v1/databases
{
"initial_data_source": {
"properties": {
// ... (Data source properties behave the same as database properties previously)
}
},
"parent": {"type": "workspace", "workspace": true} | {"type": "page_id", "page_id": "..."},
"title": [...],
"icon": {"type": "emoji", "emoji": "🚀"} | ...
}
```
```typescript Create Database with initial data source (TS SDK) theme={null}
const response = await notion.request({
method: "post",
path: "databases",
body: {
initial_data_source: {
properties: {
// ... (Data source properties behave the same as database properties previously)
}
},
},
parent: {type: "workspace", workspace: true} | {type: "page_id", page_id: "..."},
title: [...],
icon: {type: "emoji", emoji: "🚀"} | ...
}
})
// After upgrading TS SDK:
const response = await notion.databases.create({
data_source_id: dataSource.id,
})
```
#### Update database
**Before (2022-06-28):**
* In `2022-06-28`, the Update Database API was used to update attributes that related to both a database and its data source under the hood. For example, `is_inline` relates to the database, but `properties` defines the schema of a specific data source.
* **For relation properties**: across *all* API versions, *both* the `database_id` and `data_source_id` are now included in the response object.
* When providing relation properties in a request, you can either use `database_id`, `data_source_id`, or both, prior to making the API version upgrade.
* We recommend starting by switching your connection over to passing only a `data_source_id` for relation objects even in `2022-06-28` to precisely identify the data source to use for the relation and be ready for the `2025-09-03` behavior.
```json Update Database (JSON) theme={null}
// PATCH /v1/databases/:database_id
{
"icon": {
"file_upload": {"id": "..."}
},
"properties": {
"Restocked (new)": {
"type": "checkbox",
"checkbox": {}
},
"In stock": null
},
"title": [{"text": {"content": "New Title"}}]
}
```
```typescript Update Database (TS SDK) theme={null}
const response = await notion.databases.update({
database_id: "...",
icon: {file_upload: "..."},
properties: {
"Restocked (new)": {
type: "checkbox",
checkbox: {},
},
"In stock": null,
},
title: [{text: {content: "New Title"}}],
})
```
**After (2025-09-03):**
* Continue to use the Update Database API for attributes that apply to the database: `parent`, `title`, `is_inline`, `icon`, `cover`, `in_trash`.
* `parent` can be used to move an existing database to a different page, or (for public connections), to the workspace level as a private page. This is a new feature in Notion's API.
* `cover` is not supported when `is_inline` is `true`.
* Switch over to the Update *Data Source* API to modify attributes that apply to a specific data source: `properties` (to change database schema), `in_trash` (to archive or unarchive a specific data source under a database), `title`.
* Changes to one data source's `properties` doesn't affect the schema for other data source, even if they share a common database.
* **For relation properties**: You can no longer provide a `database_id`. Notion continues to include both the `database_id` and `data_source_id` in the *response* for convenience, but the *request* object (to Update Data Source) must **only contain `data_source_id`**.
Example for updating a data source's title and properties (adding one new property and removing another):
```json Update Data Source (JSON) theme={null}
// PATCH /v1/data_sources/:data_source_id
{
"properties": {
"Restocked (new)": {
"type": "checkbox",
"checkbox": {}
},
"In stock": null
},
"title": [{"text": {"content": "New Title"}}]
}
```
```typescript Update Data Source (TS SDK) theme={null}
// Update data source properties and title using SDK version
// prior to v5 and setting `notionVersion` in the `Client` to
// "2025-09-03":
const response = await notion.request({
method: "patch",
path: `data_sources/${dataSource.id}`,
data: {
properties: {
"Restocked (new)": {
type: "checkbox",
checkbox: {},
},
"In stock": null
},
},
title: [{text: {content: "New Title"}}],
})
// After upgrading TS SDK to v5:
const response = await notion.dataSources.update({
properties: {
"Restocked (new)": {
type: "checkbox",
checkbox: {},
},
"In stock": null,
},
title: [{text: {content: "New Title"}}],
})
```
Example for updating a database's parent (to move it), and switch it to be inline under the parent page:
```json Update Data Source (JSON) theme={null}
// PATCH /v1/databases/:database_id
{
"parent": {"type": "page_id", "page_id": "NEW-PAGE-ID"},
"is_inline": true
}
```
```typescript Update Data Source (TS SDK) theme={null}
const response = await notion.request({
method: "patch",
path: `databases/${DATABASE_ID}`,
body: {
parent: {type: "page_id", page_id: "NEW-PAGE-ID"},
is_inline: true,
}
})
// After upgrading TS SDK:
const response = await notion.dataSources.update({
parent: {type: "page_id", page_id: "NEW-PAGE-ID"},
is_inline: true,
})
```
### Step 4: Handle search results with data sources
**Before (2022-06-28):**
* If any Notion users add a second data source to a database, existing connections will not see any search results for that database.
**After (2025-09-03):**
* The [Search](/reference/post-search) API now only accepts `filter["value"] = "page" | "data_source"` instead of `"page" | "database"` when providing a `filter["type"] = "object"`. Make sure to update the body parameters accordingly when upgrading to `2025-09-03`.
* Currently, the search behavior remains the same. The provided query is matched against the *database* title, not the *data source* title.
* Similarly, the search API *response* returns data source IDs & objects.
* Aside from the IDs and `object: "data_source"` in these entries, the rest of the object shape of search is unchanged.
* Since results operate at the data source level, they continue to include `properties` (database schema) as before.
* If there are multiple data sources, all of them are included in the search response. Each of them will have a different data source ID.
### Step 5: Upgrade SDK (if applicable)
**Introducing `@notionhq/client` v5.0.0**
v5 of the SDK is now available:
* [NPM link](https://www.npmjs.com/package/@notionhq/client/v/5.0.0)
* [GitHub release link](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.0.0)
If you see an even newer version (e.g. `v5.0.2`) at the time you're following these steps, we recommend upgrading directly to the latest version to unlock more enhancements and bugfixes, making the upgrade smoother.
If you're using [Notion's TypeScript SDK](https://github.com/makenotion/notion-sdk-js), and have completed all of the steps above to rework your usage of Notion's endpoints to fit the `2025-09-03` suite of endpoints manually, we recommend completing the migration by upgrading to the next [major version release](https://github.com/makenotion/notion-sdk-js/releases), v5.0.0, via your `package.json` file (or other version management toolchain.)
The code snippets under Step 3 include the relevant syntax for the new `notion.dataSources.*` and `notion.databases.*` methods to assist in your upgrade. Go through each area where you used a manual `notion.request(...)` call, and switch it over to use one of the dedicated methods. Make sure you're setting the Notion version at initialization time to `2025-09-03`.
Note that the [List databases (deprecated)](/reference/get-databases) endpoint, which has been removed since version `2022-02-22`, is no longer included as of v5 of the SDK.
### Step 6: Upgrade webhooks (if applicable)
#### Introducing webhook versioning
When creating, editing, or viewing an [connection webhook subscription](/reference/webhooks) in Notion's connection settings, there's a new option to set the **API version** that applies to events delivered to your webhook URL:
For new webhook endpoints, we recommend starting with the most recent version. For existing webhook subscriptions, you'll need to carefully introduce support for the added and changed webhook types. Ensure your webhook handler can accept *both* old & new event payloads before using the "**Edit subscription**" form to upgrade to the `2025-09-03` API version.
After you've tested your webhook endpoint to ensure the new events are being handled correctly for some period of time (for example, a few hours), you can clean up your system to only expect events with the updated shape. Read on below for specific details on what's changed in `2025-09-03`.
#### New and modified event types
New `data_source` specific events have been added, and the corresponding existing `database` events now apply at the **database** level.
Here's a breakdown of how [event types](/reference/webhooks-events-delivery) change names or behavior when upgraded to `2025-09-03`:
| Old Name | New Name | Description |
| :------------------------- | :---------------------------- | :-------------------------------------------------------------------------------- |
| `database.content_updated` | `data_source.content_updated` | Data source's content updates |
| `database.schema_updated` | `data_source.schema_updated` | Data source's schema updates |
| N/A (new event) | `data_source.created` | New data source is added to an existing database `entity.type` is `"data_source"` |
| N/A (new event) | `data_source.moved` | Data source is moved to a different database `entity.type` is `"data_source"` |
| N/A (new event) | `data_source.deleted` | Data source is deleted from a database `entity.type` is `"data_source"` |
| N/A (new event) | `data_source.undeleted` | Data source is undeleted `entity.type` is `"data_source"` |
| `database.created` | (unchanged) | New database is created with a default data source |
| `database.moved` | (unchanged) | Database is moved to different parent (i.e. page) |
| `database.deleted` | (unchanged) | Database is deleted from its parent |
| `database.undeleted` | (unchanged) | Database is undeleted |
#### Updates to parent data
With the `2025-09-03` version, all webhooks for entities that can have data sources as parents now include a new field `data_source_id` under the `data.parent` object.
This applies to:
* Page events (`page.*`)
* Data source events (the `data_source.*` ones listed above)
* Database events (`database.*`), but **only** in rarer cases where databases are directly parented by another database (i.e. wikis)
For example, when a Notion user creates a page within a data source using the Notion app, the resulting `page.created` event has the following example shape (note the new `data.parent.data_source_id` field):
```json json expandable theme={null}
{
"id": "367cba44-b6f3-4c92-81e7-6a2e9659efd4",
"timestamp": "2024-12-05T23:55:34.285Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.created",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-809d-8dc4-ff2d96ae3090",
"type": "page"
},
"data": {
"parent": {
"id": "36cc9195-760f-4fff-a67e-3a46c559b176",
"type": "database",
"data_source_id": "98024f3c-b1d3-4aec-a301-f01e0dacf023"
}
}
}
```
For compatibility with multi-source databases, use the provided `parent.data_source_id` to distinguish which data source the page lives in.
## What’s Next
Read the frequently asked questions for this API change:
# Upgrade guide
Source: https://developers.notion.com/guides/get-started/upgrade-guide-2026-03-11
Learn how to upgrade your connections to 2026-03-11.
Notion API version `2026-03-11` introduces three breaking changes that affect block operations, trash/archive semantics, and the [`transcription` block type](/reference/block#transcription). Most connections will need only minor find-and-replace updates.
**Breaking changes**
If your connection uses any of the following, it will break when you upgrade to `2026-03-11`:
* The `after` parameter in [Append block children](/reference/patch-block-children)
* The `archived` field in any request or response
* The [`transcription` block type](/reference/block#transcription)
## What's changing
| Change | Before (`2025-09-03`) | After (`2026-03-11`) |
| :-------------------- | :----------------------------------------------------------- | :------------------------------------------------ |
| **Block positioning** | `after` string parameter | `position` object (`after_block`, `start`, `end`) |
| **Trash status** | `archived` field | `in_trash` field |
| **Block type rename** | [`transcription`](/reference/block#transcription) block type | `meeting_notes` block type |
## Upgrade checklist
Replace the `after` parameter with `position` in any calls to Append Block Children.
Replace all uses of the `archived` field with `in_trash` in request bodies and response handling.
Replace references to the [`transcription`](/reference/block#transcription) block type with `meeting_notes`.
Upgrade the JS/TS SDK to `v5.12.0` or later and set `notionVersion: "2026-03-11"` (if applicable).
## Step-by-step guide
### Step 1: Replace `after` with `position`
The [Append block children](/reference/patch-block-children) endpoint no longer accepts a flat `after` parameter. Instead, use the `position` object to specify where new blocks should be inserted.
The `position` object supports three placement types:
* `after_block` — insert after a specific block (replaces the old `after` parameter)
* `start` — insert at the beginning of the parent
* `end` — insert at the end of the parent (the default when `position` is omitted)
```json 2026-03-11 (after) theme={null}
// PATCH /v1/blocks/{block_id}/children
// Notion-Version: 2026-03-11
{
"position": {
"type": "after_block",
"after_block": { "id": "b5d8fd79-..." }
},
"children": [
{
"paragraph": {
"rich_text": [{ "text": { "content": "New paragraph" } }]
}
}
]
}
```
```typescript JS/TS SDK (v5.12.0+) theme={null}
import { Client } from "@notionhq/client"
const notion = new Client({
auth: process.env.NOTION_ACCESS_TOKEN,
notionVersion: "2026-03-11",
})
await notion.blocks.children.append({
block_id: "parent-block-id",
position: {
type: "after_block",
after_block: { id: "b5d8fd79-..." },
},
children: [
{
paragraph: {
rich_text: [{ text: { content: "New paragraph" } }],
},
},
],
})
```
```json 2025-09-03 (before) theme={null}
// PATCH /v1/blocks/{block_id}/children
// Notion-Version: 2025-09-03
{
"after": "b5d8fd79-...",
"children": [
{
"paragraph": {
"rich_text": [{ "text": { "content": "New paragraph" } }]
}
}
]
}
```
### Step 2: Replace `archived` with `in_trash`
The `archived` field has been renamed to `in_trash` across all API responses and request parameters. This applies to pages, databases, blocks, and data sources.
The `archived` field was [deprecated in April 2024](/page/changelog#changes-for-april-2024). If your connection already reads `in_trash` from responses, you only need to update your request parameters.
#### Response bodies
```json 2026-03-11 (after) theme={null}
{
"object": "page",
"id": "59b8df07-...",
"in_trash": false,
"created_time": "2025-08-07T10:11:07.504Z",
"last_edited_time": "2025-08-10T15:53:11.386Z",
"parent": {
"type": "page_id",
"page_id": "255104cd-..."
},
"properties": {}
}
```
```json 2025-09-03 (before) theme={null}
{
"object": "page",
"id": "59b8df07-...",
"archived": false,
"created_time": "2025-08-07T10:11:07.504Z",
"last_edited_time": "2025-08-10T15:53:11.386Z",
"parent": {
"type": "page_id",
"page_id": "255104cd-..."
},
"properties": {}
}
```
#### Request parameters
For example, when trashing a page:
```json 2026-03-11 (after) theme={null}
// PATCH /v1/pages/{page_id}
// Notion-Version: 2026-03-11
{
"in_trash": true
}
```
```typescript JS/TS SDK (v5.12.0+) theme={null}
const notion = new Client({
auth: process.env.NOTION_ACCESS_TOKEN,
notionVersion: "2026-03-11",
})
await notion.pages.update({
page_id: "59b8df07-...",
in_trash: true,
})
```
```json 2025-09-03 (before) theme={null}
// PATCH /v1/pages/{page_id}
// Notion-Version: 2025-09-03
{
"archived": true
}
```
Update both your request bodies and any code that reads `archived` from responses to use `in_trash` instead.
### Step 3: Replace `transcription` with `meeting_notes`
The [`transcription`](/reference/block#transcription) block type has been renamed to `meeting_notes`. Update any code that creates, reads, or filters by this block type.
```json 2026-03-11 (after) theme={null}
{
"object": "block",
"id": "a1c2d3e4-...",
"type": "meeting_notes",
"meeting_notes": {
"rich_text": [
{ "text": { "content": "Meeting transcript content..." } }
]
},
"created_time": "2025-10-01T12:00:00.000Z",
"last_edited_time": "2025-10-01T12:30:00.000Z",
"in_trash": false
}
```
```json 2025-09-03 (before) theme={null}
{
"object": "block",
"id": "a1c2d3e4-...",
"type": "transcription",
"transcription": {
"rich_text": [
{ "text": { "content": "Meeting transcript content..." } }
]
},
"created_time": "2025-10-01T12:00:00.000Z",
"last_edited_time": "2025-10-01T12:30:00.000Z",
"in_trash": false
}
```
If your connection filters blocks by type, update any `type === "transcription"` checks to use `"meeting_notes"`.
### Step 4: Upgrade the JS/TS SDK (if applicable)
**`@notionhq/client` v5.12.0**
[`v5.12.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.12.0) of the SDK adds backwards-compatible support for API version `2026-03-11`. All old fields and types are preserved with `@deprecated` annotations — no breaking changes.
To opt in to the new version:
```typescript theme={null}
import { Client } from "@notionhq/client"
const notion = new Client({
auth: process.env.NOTION_ACCESS_TOKEN,
notionVersion: "2026-03-11",
})
```
The SDK's TypeScript types include both the old and new field names during the transition period. The old names (`archived`, `after`, `transcription`) are marked `@deprecated` to help you find code that needs updating.
### Database automation webhooks
If you use database automation webhooks (the "Send webhook" action in Notion automations), Notion will display an upgrade banner in the automation editor when a webhook action uses an older API version. You can upgrade individual webhook actions to `2026-03-11` using the "Upgrade to latest version" button, or leave them on `2025-09-03` for backward compatibility.
New webhook actions will default to `2026-03-11` going forward.
### Connection webhooks
API version `2026-03-11` is now available as an [connection webhook](/reference/webhooks) subscription version. While there are no changes to webhook event payloads in this version (the `archived → in_trash` rename only applies to REST API request/response bodies, not webhook payloads), we recommend upgrading your webhook subscription version to `2026-03-11` to keep it in sync with your API version.
To upgrade your webhook subscription version:
Go to the Developer portal and select your connection.
Navigate to the **Webhooks** tab.
Click the **Edit subscription** button on your webhook subscription.
In the **API version** dropdown, select `2026-03-11`.
Click **Update subscription** to save.
This is a no-op upgrade — webhook event payloads are identical between `2025-09-03` and `2026-03-11`. The new version is provided for consistency with the REST API version.
# Introduction
Source: https://developers.notion.com/guides/link-previews/introduction
Learn how link previews work in Notion.
A Link Preview is a real-time excerpt of authenticated content that unfurls in Notion when an authenticated user shares an enabled link. Instead of logging in to multiple tools at a time, collaborators can use Link Previews to centralize their work in Notion.
Notion supports Link Previews for selected partner services. For example:
* **Trello** links can unfurl information about a linked task.
* **Figma** links can share a linked board’s image preview and corresponding metadata.
* **Amplitude** links can share a linked graph in an iFrame along with an interface to modify the graph.
* **Slack** links can unfurl a linked message’s content and author.
## How Link Previews work
A user shares a Link Preview enabled URL. Notion detects supported URLs based on the Link Preview connection settings for that service. If it’s the first time that a user has shared a supported URL, then Notion kicks off an auth flow to authenticate with the source service. After the user authenticates, Notion and the source service exchange tokens that allow Notion to show a Link Preview in the user’s workspace.
The source service also detects any changes to the data embedded in the Link Preview and alerts Notion when the Link Preview needs to be updated.
Notion alerts the source service when a Link Preview is deleted, so that the service can stop listening for updates.
## Link Previews vs. Embed blocks
If you have used [Embed blocks](/reference/block#embed) in Notion’s UI before, you may be wondering how Link Previews differ from them. Embeds allow Notion users to embed online content, such as a webpage, PDF, and more, directly in a Notion page. This allows users to preview the content without leaving Notion.
Link Previews are similar, but they display authenticated, structured content from a supported service. Rather than embedding the full content of a webpage or file being shared, Link Previews pull data from a linked page and display it in an unfurled format.
Since Link preview connections require [OAuth 2.0](https://www.oauth.com/) authentication, unfurled link content will update as the data being shared updates. For example, if a GitHub pull request is shared as a Link Preview, the data displayed in the preview will update as the pull request updates (e.g. when it is merged).
To learn more about Embed blocks, read our [reference docs](/reference/block#embed) and [Help Centre guide](https://www.notion.com/help/embed-and-connect-other-apps).
To learn more about Link Previews, see the following resources:
* [API reference docs for the Link Preview unfurl attribute object](/reference/unfurl-attribute-object)
* [Help Centre](https://www.notion.com/help/guides/notion-api-link-previews-feature) guide
# Build an MCP client for Notion
Source: https://developers.notion.com/guides/mcp/build-mcp-client
Connect a custom MCP client to Notion MCP with OAuth 2.0 and PKCE.
Build an
[MCP client](https://modelcontextprotocol.io/docs/develop/build-client) that
connects to [Notion MCP](/guides/mcp/overview) using OAuth 2.0 with
[PKCE](https://oauth.net/2/pkce/).
## Connection requirements
Notion provides a hosted
[MCP (Model Context Protocol)](https://modelcontextprotocol.io/introduction)
server that lets MCP clients work with Notion workspaces. The server is
available at:
| Transport | URL | Notes |
| --------------------------------- | ---------------------------- | ---------------------------------------------- |
| **Streamable HTTP** (recommended) | `https://mcp.notion.com/mcp` | Recommended for new clients |
| **Server-Sent Events (SSE)** | `https://mcp.notion.com/sse` | For clients that don't support Streamable HTTP |
Both endpoints support the same MCP protocol and OAuth authentication. Your
client should try Streamable HTTP first and fall back to SSE if needed.
Your client must support:
* OAuth 2.0 Authorization Code flow with PKCE
* Support for Streamable HTTP (`/mcp`) or SSE (`/sse`) transports
* Token refresh handling
* Secure credential storage
## Prerequisites
This guide uses TypeScript/JavaScript examples, but the concepts apply to any
programming language. The OAuth 2.0 flow, PKCE implementation, and MCP
protocol are language-agnostic.
**Required libraries (TypeScript/JavaScript):**
```bash npm theme={null}
npm install @modelcontextprotocol/sdk
npm install oauth # or openid-client
```
```bash TypeScript types theme={null}
npm install --save-dev @types/node
```
### Alternative libraries for other languages
* **MCP SDK**: [python-sdk](https://github.com/modelcontextprotocol/python-sdk) (official)
* **OAuth 2.0**: [`authlib`](https://docs.authlib.org) (recommended) or [`requests-oauthlib`](https://requests-oauthlib.readthedocs.io)
* **PKCE**: Built into both `authlib` and `requests-oauthlib`
* **MCP SDK**: [go-sdk](https://github.com/modelcontextprotocol/go-sdk) (official)
* **OAuth 2.0**: [`golang.org/x/oauth2`](https://pkg.go.dev/golang.org/x/oauth2) (official extended package)
* **PKCE**: Supported via `oauth2.SetAuthURLParam("code_challenge", ...)` and `oauth2.SetAuthURLParam("code_challenge_method", "S256")`
* **MCP SDK**: [rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) (official)
* **OAuth 2.0**: [`oauth2`](https://docs.rs/oauth2) crate
* **PKCE**: Built into `oauth2` crate via `PkceCodeChallenge` and `PkceCodeVerifier`
* **MCP SDK**: Use HTTP client libraries (Apache HttpClient, OkHttp, or Java 11+ `HttpClient`)
* **OAuth 2.0**: [`Spring Security OAuth2`](https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html) (recommended) or [`ScribeJava`](https://github.com/scribejava/scribejava)
* **PKCE**: Built into Spring Security OAuth2 Client; supported in ScribeJava via `PKCE` configuration
* **MCP SDK**: Use `HttpClient` with [`System.Net.Http.Json`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.json)
* **OAuth 2.0**: [`IdentityModel.OidcClient`](https://github.com/IdentityModel/IdentityModel.OidcClient) or [`Microsoft.Identity.Web`](https://learn.microsoft.com/en-us/azure/active-directory/develop/microsoft-identity-web)
* **PKCE**: Built into both libraries
* **MCP SDK**: Use `Net::HTTP` (standard library) or [`Faraday`](https://lostisland.github.io/faraday/)
* **OAuth 2.0**: [`oauth2`](https://github.com/oauth-xx/oauth2) gem
* **PKCE**: Supported via `oauth2` gem with appropriate configuration
### Key references
* [MCP Specification](https://spec.modelcontextprotocol.io) — Model Context Protocol standard
* [Building an MCP client](https://modelcontextprotocol.io/docs/develop/build-client)
* [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) — OAuth 2.0 Authorization Framework
* [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) — PKCE (Proof Key for Code Exchange)
* [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) — OAuth 2.0 Authorization Server Metadata
* [RFC 9470](https://datatracker.ietf.org/doc/html/rfc9470) — OAuth 2.0 Protected Resource Metadata
## Step 1: OAuth discovery
Before connecting to an MCP server, discover its OAuth configuration. Given the
MCP server URL (e.g., `https://mcp.notion.com/mcp`), use a standard two-step
discovery process:
1. **RFC 9470**: Fetch Protected Resource Metadata to find which authorization
server(s) protect this resource
2. **RFC 8414**: Fetch Authorization Server Metadata to get OAuth endpoints
### Understanding the discovery flow
An MCP server (the protected resource) might be hosted at `mcp.example.com` but
delegate authentication to a separate OAuth server at `auth.example.com`. The
Protected Resource Metadata tells you where to find the authorization server,
and the Authorization Server Metadata tells you the specific OAuth endpoints to
use.
### Standard discovery implementation
Here's a function that implements the complete RFC 9470 → RFC 8414 discovery
flow:
```typescript TypeScript theme={null}
type OAuthMetadata = {
issuer: string
authorization_endpoint: string
token_endpoint: string
registration_endpoint?: string
code_challenge_methods_supported?: string[]
grant_types_supported?: string[]
response_types_supported?: string[]
scopes_supported?: string[]
}
/**
* Discovers OAuth configuration for an MCP server using RFC 9470 + RFC 8414.
*/
async function discoverOAuthMetadata(
mcpServerUrl: string
): Promise {
const url = new URL(mcpServerUrl)
const protectedResourceUrl = new URL(
"/.well-known/oauth-protected-resource",
url
)
// Step 1: RFC 9470 - Get Protected Resource Metadata
const protectedResourceResponse = await fetch(
protectedResourceUrl.toString()
)
if (!protectedResourceResponse.ok) {
throw new Error(
`Failed to fetch protected resource metadata: ` +
`${protectedResourceResponse.status}`
)
}
const protectedResource = await protectedResourceResponse.json()
const authServers = protectedResource.authorization_servers
if (!Array.isArray(authServers) || authServers.length === 0) {
throw new Error(
"No authorization servers found in protected resource metadata"
)
}
// Use the first authorization server
const authServerUrl = authServers[0]
// Step 2: RFC 8414 - Get Authorization Server Metadata
const metadataUrl = new URL(
"/.well-known/oauth-authorization-server",
authServerUrl
)
const metadataResponse = await fetch(metadataUrl.toString())
if (!metadataResponse.ok) {
throw new Error(
`Failed to fetch authorization server metadata: ` +
`${metadataResponse.status}`
)
}
const metadata = (await metadataResponse.json()) as OAuthMetadata
// Validate required fields
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
throw new Error("Missing required OAuth endpoints in metadata")
}
// Warn if PKCE support isn't advertised
if (!metadata.code_challenge_methods_supported?.includes("S256")) {
console.warn(
"Server does not advertise S256 PKCE support, " +
"but we will use it anyway"
)
}
return metadata
}
```
**What this does:**
1. Fetches Protected Resource Metadata from
`https://mcp.notion.com/mcp/.well-known/oauth-protected-resource`
— Returns: `{ "authorization_servers": ["https://..."], ... }`
2. Extracts the authorization server URL from the `authorization_servers` array
3. Fetches Authorization Server Metadata from
`{authServerUrl}/.well-known/oauth-authorization-server`
— Returns: OAuth endpoints like `authorization_endpoint`, `token_endpoint`,
etc.
4. Validates that all required fields are present and warns if PKCE support
isn't advertised
This approach is universal and works for any MCP server that follows RFC 9470
and RFC 8414 standards, not just Notion's MCP server.
## Step 2: Generate PKCE parameters
PKCE (Proof Key for Code Exchange) is mandatory for secure OAuth flows.
Generate a code verifier and challenge:
```typescript TypeScript theme={null}
import { randomBytes, createHash } from "crypto"
function base64URLEncode(str: Buffer): string {
return str
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "")
}
function generateCodeVerifier(): string {
// Generate 32 random bytes = 256 bits
// Base64 encoding produces ~43 characters
const bytes = randomBytes(32)
return base64URLEncode(bytes)
}
function generateCodeChallenge(verifier: string): string {
const hash = createHash("sha256").update(verifier).digest()
return base64URLEncode(hash)
}
// Usage
const codeVerifier = generateCodeVerifier()
const codeChallenge = generateCodeChallenge(codeVerifier)
// Store codeVerifier securely - you'll need it for token exchange
```
The `codeVerifier` must be kept secret and never sent to the authorization
server until the token exchange step. Store it securely (encrypted session,
secure cookie, or in-memory with short expiry).
## Step 3: Dynamic client registration
Notion MCP server supports dynamic client registration (RFC 7591). Check if
`registration_endpoint` exists in the metadata:
```typescript TypeScript theme={null}
type ClientRegistration = {
client_name: string
client_uri?: string
redirect_uris: string[]
grant_types: string[]
response_types: string[]
token_endpoint_auth_method: string
scope?: string
}
type ClientCredentials = {
client_id: string
client_secret?: string
client_id_issued_at?: number
client_secret_expires_at?: number
}
async function registerClient(
metadata: OAuthMetadata,
redirectUri: string
): Promise {
if (!metadata.registration_endpoint) {
throw new Error("Server does not support dynamic client registration")
}
const registrationRequest: ClientRegistration = {
client_name: "Your MCP Client",
client_uri: "https://example.com",
redirect_uris: [redirectUri],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: "none",
}
const response = await fetch(metadata.registration_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(registrationRequest),
})
if (!response.ok) {
const errorBody = await response.text()
throw new Error(
`Client registration failed: ${response.status} - ${errorBody}`
)
}
const credentials = (await response.json()) as ClientCredentials
// Store credentials securely
return credentials
}
```
## Step 4: Initiate authorization flow
Redirect the user to the authorization endpoint with PKCE parameters:
```typescript TypeScript theme={null}
function buildAuthorizationUrl(
metadata: OAuthMetadata,
clientId: string,
redirectUri: string,
codeChallenge: string,
state: string,
scopes: string[] = []
): string {
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
scope: scopes.join(" "),
state: state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
prompt: "consent",
})
return `${metadata.authorization_endpoint}?${params.toString()}`
}
function generateState(): string {
return randomBytes(32).toString("hex")
}
// Usage
const state = generateState()
const authorizationUrl = buildAuthorizationUrl(
metadata,
clientId,
redirectUri,
codeChallenge,
state
)
// Store state and codeVerifier in secure session storage
// Redirect user to authorizationUrl
window.location.href = authorizationUrl // Browser redirect
```
**Security best practices:**
* Always use HTTPS for redirect URIs in production
* Store `state` and `codeVerifier` securely (encrypted session storage)
* Set a short expiry (10 minutes) for stored values
* Validate `state` on callback to prevent CSRF attacks
## Step 5: Handle OAuth callback
After user authorizes, they'll be redirected back to your `redirectUri` with an
authorization code:
```typescript TypeScript theme={null}
interface CallbackParams {
code?: string
state?: string
error?: string
error_description?: string
}
function parseCallback(url: string): CallbackParams {
const urlParams = new URLSearchParams(new URL(url).search)
return {
code: urlParams.get("code") || undefined,
state: urlParams.get("state") || undefined,
error: urlParams.get("error") || undefined,
error_description: urlParams.get("error_description") || undefined,
}
}
async function handleCallback(
callbackUrl: string,
storedState: string,
codeVerifier: string
): Promise {
const params = parseCallback(callbackUrl)
if (params.error) {
throw new Error(
`OAuth error: ${params.error} - ` +
`${params.error_description || "Unknown error"}`
)
}
if (params.state !== storedState) {
throw new Error("Invalid state parameter - possible CSRF attack")
}
if (!params.code) {
throw new Error("Missing authorization code")
}
return params.code
}
```
## Step 6: Exchange authorization code for tokens
Exchange the authorization code for access and refresh tokens:
```typescript TypeScript theme={null}
type TokenResponse = {
access_token: string
token_type: string
expires_in?: number
refresh_token?: string
scope?: string
// Identity fields, present on successful authorization-code exchanges
user_id?: string
workspace_id?: string
email_domain?: string
}
async function exchangeCodeForTokens(
code: string,
codeVerifier: string,
metadata: OAuthMetadata,
clientId: string,
clientSecret: string | undefined,
redirectUri: string
): Promise {
const params = new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: clientId,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
})
if (clientSecret) {
params.append("client_secret", clientSecret)
}
const response = await fetch(metadata.token_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
"User-Agent": "YourApp-MCP-Client/1.0",
},
body: params.toString(),
})
if (!response.ok) {
const errorBody = await response.text()
throw new Error(
`Token exchange failed: ${response.status} - ${errorBody}`
)
}
const tokens = await response.json()
if (!tokens.access_token) {
throw new Error("Missing access_token in response")
}
return tokens
}
```
**Identity fields in the token response**
Successful authorization-code exchanges also return `user_id` and
`workspace_id`, the Notion IDs of the authorizing user and workspace, plus
`email_domain`, the lowercased domain of the authorizing user's email
address. Use them to associate the connection with a user and workspace
without an extra call. Refresh responses don't include these fields, so
store them from the initial exchange.
Notion may add fields to the token response over time. Parse it leniently
and [ignore fields you don't recognize](/reference/versioning#what-we-consider-backwards-compatible).
**Token storage security:**
* **Web applications:** Store tokens server-side only, never in localStorage
or cookies
* **Desktop applications:** Use secure credential storage (Keychain on macOS,
Credential Manager on Windows)
* **Mobile applications:** Use secure keychain/keystore APIs
* Always encrypt tokens at rest
## Step 7: Connect to MCP server with authentication
Notion's MCP server supports two transport protocols. Your client should try
Streamable HTTP first and automatically fall back to SSE if needed.
```typescript TypeScript theme={null}
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import {
StreamableHTTPClientTransport
} from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import {
SSEClientTransport
} from "@modelcontextprotocol/sdk/client/sse.js"
async function createMcpClient(
serverUrl: string,
accessToken: string,
useSSE: boolean = false
): Promise {
const client = new Client(
{
name: "your-mcp-client",
version: "1.0.0",
},
{
capabilities: {
roots: {},
sampling: {},
},
}
)
let transport
if (useSSE) {
transport = new SSEClientTransport(new URL(`${serverUrl}/sse`), {
requestInit: {
headers: {
Authorization: `Bearer ${accessToken}`,
"User-Agent": "YourApp-MCP-Client/1.0",
},
},
})
} else {
transport = new StreamableHTTPClientTransport(
new URL(`${serverUrl}/mcp`),
{
requestInit: {
headers: {
Authorization: `Bearer ${accessToken}`,
"User-Agent": "YourApp-MCP-Client/1.0",
},
},
}
)
}
await client.connect(transport)
return client
}
// Usage with automatic fallback
async function connectToNotionMcp(accessToken: string): Promise {
const serverUrl = "https://mcp.notion.com"
try {
return await createMcpClient(serverUrl, accessToken, false)
} catch (error) {
console.warn("Streamable HTTP failed, falling back to SSE:", error)
return await createMcpClient(serverUrl, accessToken, true)
}
}
```
### Identify the connected workspace
The OAuth token response includes `user_id` and `workspace_id`, but not
display names, and the public REST API's `GET /v1/users/me` does not accept
MCP-audienced tokens. To label a connection with the workspace name after
connecting, call the `fetch` tool with the special id `self`:
```typescript TypeScript theme={null}
const result = await client.callTool({
name: "notion-fetch", // "fetch" for OpenAI clients (the notion- prefix is dropped)
arguments: { id: "self" },
})
// Tool results come back as MCP content blocks, not as a typed object. The Notion
// MCP server returns the tool's JSON payload as a single text block, so parse it
// and read the `self` field from the result.
const [block] = result.content
if (block?.type !== "text") {
throw new Error("Expected a text content block from notion-fetch")
}
const { workspace, user, current_tool_access } = JSON.parse(block.text).self
console.log(`Connected to ${workspace.name} (${workspace.id}) as ${user.name}`)
// workspace: { id, name }
// user: { id, name, type, email }
// current_tool_access: { [toolName]: { status, upgrade_url? } }
```
The `self` payload also carries a `current_tool_access` map, so an MCP client
can tell up front which tools will actually run on the connected workspace's
plan and which would only return an upgrade prompt. Each entry's `status` is
`available`, `available_with_limit` (calls can be made up to the limit included with
the workspace's plan), `upgrade_required` (calls return an upgrade prompt, and the entry
includes an `upgrade_url` into the workspace's upgrade flow), or `not_enabled`.
See [Supported tools](/guides/mcp/mcp-supported-tools) for details.
## Step 8: Handle token refresh
Access tokens expire. Implement automatic refresh with proper error handling:
```typescript TypeScript expandable theme={null}
async function refreshAccessToken(
refreshToken: string,
metadata: OAuthMetadata,
clientId: string,
clientSecret: string | undefined
): Promise {
const params = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
})
if (clientSecret) {
params.append("client_secret", clientSecret)
}
const response = await fetch(metadata.token_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: params.toString(),
})
if (!response.ok) {
const errorBody = await response.text()
try {
const error = JSON.parse(errorBody)
if (error.error === "invalid_grant") {
throw new Error("REAUTH_REQUIRED")
}
if (error.error === "invalid_client") {
throw new Error("INVALID_CLIENT")
}
} catch (parseError) {
// Not JSON error response
}
throw new Error(
`Token refresh failed: ${response.status} - ${errorBody}`
)
}
const tokens = await response.json()
return tokens
}
```
Many servers rotate refresh tokens for security (RFC 6749 Section 10.4).
Always store the new `refresh_token` if provided in the response.
## Complete example
Here's a complete class that ties all the steps together:
```typescript TypeScript expandable theme={null}
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import {
StreamableHTTPClientTransport
} from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import {
SSEClientTransport
} from "@modelcontextprotocol/sdk/client/sse.js"
import { randomBytes, createHash } from "crypto"
class NotionMcpClient {
private serverUrl = "https://mcp.notion.com"
private metadata!: OAuthMetadata
private clientId!: string
private clientSecret?: string
private accessToken?: string
private refreshToken?: string
private client?: Client
async initialize(redirectUri: string): Promise {
this.metadata = await discoverOAuthMetadata(this.serverUrl)
const credentials = await registerClient(this.metadata, redirectUri)
this.clientId = credentials.client_id
this.clientSecret = credentials.client_secret
}
async startAuthFlow(redirectUri: string): Promise {
const codeVerifier = generateCodeVerifier()
const codeChallenge = generateCodeChallenge(codeVerifier)
const state = generateState()
// Store these securely
this.storeSecurely("codeVerifier", codeVerifier)
this.storeSecurely("state", state)
return buildAuthorizationUrl(
this.metadata,
this.clientId,
redirectUri,
codeChallenge,
state
)
}
async handleCallback(
callbackUrl: string,
redirectUri: string
): Promise {
const storedState = this.retrieveSecurely("state")
const codeVerifier = this.retrieveSecurely("codeVerifier")
const code = await handleCallback(
callbackUrl,
storedState,
codeVerifier
)
const tokens = await exchangeCodeForTokens(
code,
codeVerifier,
this.metadata,
this.clientId,
this.clientSecret,
redirectUri
)
this.accessToken = tokens.access_token
this.refreshToken = tokens.refresh_token
// Clean up stored values
this.deleteSecurely("state")
this.deleteSecurely("codeVerifier")
}
async connect(): Promise {
if (!this.accessToken) {
throw new Error("Not authenticated")
}
try {
this.client = await createMcpClient(
this.serverUrl,
this.accessToken,
false
)
} catch (error) {
console.warn("Streamable HTTP failed, falling back to SSE")
this.client = await createMcpClient(
this.serverUrl,
this.accessToken,
true
)
}
return this.client
}
async ensureValidToken(): Promise {
if (!this.refreshToken) {
throw new Error("No refresh token available")
}
try {
const tokens = await refreshAccessToken(
this.refreshToken,
this.metadata,
this.clientId,
this.clientSecret
)
this.accessToken = tokens.access_token
if (tokens.refresh_token) {
this.refreshToken = tokens.refresh_token
}
} catch (error) {
if (
error instanceof Error &&
error.message === "REAUTH_REQUIRED"
) {
throw new Error("Re-authentication required")
}
throw error
}
}
private storeSecurely(key: string, value: string): void {
// Implement secure storage
}
private retrieveSecurely(key: string): string {
// Implement secure retrieval
return ""
}
private deleteSecurely(key: string): void {
// Implement secure deletion
}
}
```
## Security best practices
1. **Always use HTTPS** — Never use HTTP except for localhost development
2. **Validate state parameter** — Always verify state matches stored value on
callback
3. **Secure token storage** — Encrypt tokens at rest, never expose to
client-side code
4. **PKCE is mandatory** — Always use PKCE even if server doesn't advertise
support
5. **Token expiry handling** — Check token expiry before each request, refresh
proactively
6. **Error handling** — Handle `invalid_grant` errors gracefully
(re-authentication required)
7. **HTTPS verification** — Validate SSL certificates in production
8. **Rate limiting** — Implement rate limiting for token refresh to prevent
abuse
9. **Scope minimization** — Only request the scopes you actually need
10. **Audit logging** — Log all OAuth operations for security auditing
## Troubleshooting
* Store the state securely and validate on callback
* Expire after \~10 minutes
* Use the exact verifier that produced the code\_challenge
* Ensure base64url encoding (no +, /, =)
* Use RFC 9470 (protected resource) then RFC 8414 (authorization server)
* Confirm server supports OAuth and your URL is correct
* Prefer Streamable HTTP; fall back to SSE
* Check proxy or firewall rules
* Do token exchange server-side; browser should only handle redirects
When refresh returns `{ "error": "invalid_grant" }`, the refresh token is
invalid, expired, revoked, or superseded by rotation. Do not retry refresh;
prompt re-authentication.
**Common causes:**
1. **Rotation on use** — Providers rotate refresh tokens and revoke the old
one. **Fix:** Persist the new `refresh_token` atomically with the access
token.
2. **Expired refresh token** — Often 30–90 days. **Fix:** Re-authenticate;
monitor early expirations.
3. **Client credential mismatch** — `client_id` or `client_secret` differs
from initial auth. **Fix:** Keep credentials consistent for the token's
lifetime.
4. **Explicit revocation or policy event** — User revoked access, password
change, or security policy. **Fix:** Show clear reconnect UI.
5. **Concurrent refreshes** — Parallel refreshes cause losers to see
`invalid_grant`. **Fix:** Use a mutex or distributed lock around refresh.
**Operational guidance:**
* `invalid_grant` → re-authenticate
* `temporarily_unavailable` or network errors → retry with backoff
* Refresh 5–10 minutes before expiry to avoid races
* Cache access tokens with accurate expiry
### Notion MCP OAuth specifics
Notion's remote MCP server is built on
[Cloudflare's `workers-oauth-provider` package](https://github.com/cloudflare/workers-oauth-provider).
Its token lifecycle has a few behaviors worth designing your client around. The
guarantees below are the ones you can rely on; build for them as the strictest
case.
#### Token lifecycle
* **Access tokens** currently last about **eight hours**, but this duration is
subject to change. Always use the token response's `expires_in` value and
refresh proactively rather than hard-coding a lifetime.
* **Refresh tokens** expire under either of two conditions, whichever comes
first: an absolute maximum lifetime of **180 days** measured from when the user
first authorized the connection (this cap does not slide — refreshing does not
extend it), or **30 consecutive days of inactivity** (no successful refresh).
An actively used connection keeps working until the 180-day mark; an idle one
lapses after 30 days. In either case the next refresh returns `invalid_grant`
and the user must re-authorize. Treat periodic reconnection as expected, not
exceptional, and make sure your reconnect flow is easy to reach.
For reliable long-lived connections:
* Persist dynamic client registration credentials (`client_id` and
`client_secret`) durably and reuse them, because re-registering orphans prior
grants. Alternatively, use a Client ID Metadata Document (CIMD), which Notion
MCP supports.
* Persist each rotated `refresh_token` atomically and serialize refreshes per
grant. Never refresh the same grant concurrently from multiple processes.
* Treat `invalid_grant` as terminal: clear the stored tokens and reauthorize
once. Do not retry-loop.
#### Refresh token rotation
Every refresh rotates the refresh token: the token response returns a new
`refresh_token`, and the one you sent is retired. To keep a connection healthy:
* Persist the new `refresh_token` from each refresh response atomically with the
new access token, before you issue the next request.
* A grant keeps at most two refresh tokens valid at once — the current one and
the immediately previous one (a one-step window). If a transient failure
prevents you from storing a rotated token, you may retry once with the
previously stored token.
**Reusing a refresh token that has already been rotated away can revoke the
entire connection.** As a theft-containment measure, replaying a refresh token
that was rotated out more than a brief grace period earlier is treated as a
stolen-token signal: the server revokes the whole grant. Every access and
refresh token for that connection stops working, and the user must
re-authorize from scratch. To stay clear of it:
* Serialize refreshes per connection with a mutex or distributed lock. Never
refresh the same connection from two workers or replicas concurrently —
distributed setups that share a connection without a consistent, atomic
token store are the most common cause of accidental reuse.
* Treat `invalid_grant` as terminal for the connection: drop the stored tokens
and surface re-authentication. Do **not** retry a refresh that returned
`invalid_grant`. A revoked or expired grant cannot recover, and retry loops
against it only generate load and never succeed.
#### Handling `invalid_grant`
`invalid_grant` from the token endpoint always means the connection is dead and
the user must reconnect, whether the cause is refresh-token reuse, the 30-day
lifetime, an explicit revocation, or a credential mismatch. Stop refreshing that
connection, clear its tokens, and prompt re-authorization. See
[Troubleshooting](#troubleshooting) above for the full list of causes and fixes.
## Optional: MCP server discovery via `mcp.json`
The `mcp.json` convention described here is not part of the MCP specification.
It is an unofficial convention, championed by Notion and Cursor, that MCP
clients can optionally support to improve the user experience.
MCP clients can discover available MCP servers by checking for a
`/.well-known/mcp.json` file on a website's domain. When a user pastes a link,
the client can detect the MCP server and offer to connect to it.
For example, Notion hosts its discovery file at:
```
https://www.notion.com/.well-known/mcp.json
```
The file contains:
```json theme={null}
{
"name": "Notion",
"description": "Connect your Notion workspace to search, update, and trigger workflows across tools.",
"icon": "https://www.notion.com/images/notion-logo-block-main.svg",
"endpoint": "https://mcp.notion.com/mcp"
}
```
### Schema
| Field | Type | Description |
| :------------ | :------------- | :-------------------------------------------- |
| `name` | `string` | Human-readable name of the MCP server |
| `description` | `string` | Brief description of what the server provides |
| `icon` | `string` (URL) | URL to the server's icon |
| `endpoint` | `string` (URL) | The MCP server endpoint URL |
### How to use this in your client
When a user pastes a URL (e.g., `https://www.notion.com/some-page`), your client
can check for `/.well-known/mcp.json` on that domain. If the file exists, your
client can:
1. **Show a prompt** suggesting the user connect to the MCP server for richer
interaction
2. **Auto-connect** if the user has previously authorized the server
3. **Use the MCP server** to fetch structured data instead of scraping the web
page
```typescript theme={null}
async function discoverMcpServer(
url: string
): Promise<{ name: string; endpoint: string } | null> {
try {
const origin = new URL(url).origin
const response = await fetch(
`${origin}/.well-known/mcp.json`
)
if (!response.ok) return null
return await response.json()
} catch {
return null
}
}
```
### Publishing your own `mcp.json`
If you operate an MCP server, you can publish a `mcp.json` file at
`/.well-known/mcp.json` on your domain so that MCP clients can discover your
server automatically.
## Additional resources
* [Notion MCP Server GitHub](https://github.com/makenotion/notion-mcp-server)
* [MCP SDK Documentation](https://github.com/modelcontextprotocol/sdk)
* [MCP Registry](https://github.com/modelcontextprotocol/registry/tree/main/docs) — Anthropic's MCP server registry
# Common MCP clients
Source: https://developers.notion.com/guides/mcp/common-mcp-clients
Find setup documentation for MCP clients that can connect to Notion MCP.
These MCP clients can connect to Notion MCP. Follow the link for your client to learn how to add a remote MCP server.
Some clients include a directory or marketplace where you can add **Notion**. For setup instructions, see [Connect to Notion MCP](/guides/mcp/get-started-with-mcp).
For the [local MCP server](/guides/mcp/hosting-open-source-mcp), see the [README for connection instructions](https://github.com/makenotion/notion-mcp-server?tab=readme-ov-file#3-adding-mcp-config-to-your-client).
Building your own MCP client? See [Build an MCP client for Notion](/guides/mcp/build-mcp-client) for OAuth and connection requirements.
# Connect to Notion MCP
Source: https://developers.notion.com/guides/mcp/get-started-with-mcp
Connect an MCP client to your Notion workspace.
Follow the instructions for your MCP client. After you authorize the connection, the client can read and update content that you can access in the selected Notion workspace.
## Claude Code
Run this command in your terminal:
```bash theme={null}
claude mcp add --transport http notion https://mcp.notion.com/mcp
```
Then authenticate by running `/mcp` in Claude Code and following the OAuth flow.
* `--scope local` (default): Available only to you in the current project
* `--scope project`: Shared with your team via `.mcp.json` file
* `--scope user`: Available to you across all projects
Use the `/mcp` command to list and manage the MCP servers you have installed, and use the `/context` command to understand the context token usage of your current session, including the number of tokens used by each MCP server that's enabled.
Install the [Notion plugin for Claude Code](https://github.com/makenotion/claude-code-notion-plugin) to add the MCP server, Skills, and slash commands for common Notion workflows.
## Cursor
Open **Cursor Settings** → **MCP** → **Add new global MCP server**
Paste the following configuration:
```json theme={null}
{
"mcpServers": {
"notion": {
"url": "https://mcp.notion.com/mcp"
}
}
}
```
Save and restart Cursor. When you use a Notion tool for the first time, complete the OAuth flow to connect your workspace.
To share the Notion MCP configuration with your team, create a `.cursor/mcp.json` file in your project root:
```json theme={null}
{
"mcpServers": {
"notion": {
"url": "https://mcp.notion.com/mcp"
}
}
}
```
## VS Code (GitHub Copilot)
Create a `.vscode/mcp.json` file in your workspace:
```json theme={null}
{
"servers": {
"notion": {
"type": "http",
"url": "https://mcp.notion.com/mcp"
}
}
}
```
Open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) and run **MCP: List Servers**
Start the Notion server and complete the OAuth flow when prompted
To configure Notion MCP across all workspaces, run **MCP: Open User Configuration** from the Command Palette and add the server configuration there.
## Claude Desktop
Open **Settings** → **Connectors**
Select **Add Connector** and enter the URL:
```
https://mcp.notion.com/mcp
```
Complete the OAuth flow to connect your Notion workspace
Remote MCP servers in Claude Desktop are configured through Settings → Connectors, not the `claude_desktop_config.json` file. Available on Pro, Max, Team, and Enterprise plans.
## Windsurf
Open **Windsurf Settings** (`Cmd+,` on Mac) → search for **MCP**
Select **View raw config** to open `mcp_config.json`
Add the Notion server configuration:
```json theme={null}
{
"mcpServers": {
"notion": {
"serverUrl": "https://mcp.notion.com/mcp"
}
}
}
```
Save and restart Windsurf. Complete the OAuth flow when prompted.
## ChatGPT
Go to [chatgpt.com/#settings/Connectors](https://chatgpt.com/#settings/Connectors) (requires login)
Select **Add Connector** and enter the URL:
```
https://mcp.notion.com/mcp
```
Complete the OAuth flow to connect your Notion workspace
## Codex
For more details, see the [Codex MCP documentation](https://developers.openai.com/codex/mcp/).
Add the Notion server to your Codex configuration at `~/.codex/config.toml`:
```toml theme={null}
[mcp_servers.notion]
url = "https://mcp.notion.com/mcp"
```
Authenticate by running:
```bash theme={null}
codex mcp login notion
```
Complete the OAuth flow to connect your Notion workspace.
To share the Notion MCP configuration with your team, create a `.codex/config.toml` file in your project root with the same server configuration.
## Antigravity
We recommend connecting to Notion MCP as a custom server rather than using the pre-configured "Notion" connector in the Antigravity MCP gallery, which uses the deprecated [`notion-mcp-server`](https://github.com/makenotion/notion-mcp-server) package.
Follow the [Antigravity instructions for connecting custom MCP servers](https://antigravity.google/docs/mcp#connecting-custom-mcp-servers) and add the following to your `mcp_config.json`:
```json theme={null}
{
"mcpServers": {
"notion": {
"serverUrl": "https://mcp.notion.com/mcp"
}
}
}
```
Save the configuration. Antigravity will prompt you to complete the OAuth flow to connect your Notion workspace.
## Other MCP clients
If your MCP client isn't listed above, use one of these URLs:
| Transport | URL | Notes |
| :-------------------------------- | :--------------------------- | :--------------------------------------------- |
| **Streamable HTTP** (recommended) | `https://mcp.notion.com/mcp` | Recommended for new clients |
| **SSE** (Server-Sent Events) | `https://mcp.notion.com/sse` | For clients that don't support Streamable HTTP |
### JSON configuration format
Most MCP clients accept a JSON configuration. Use the format supported by your client:
```json Streamable HTTP theme={null}
{
"mcpServers": {
"notion": {
"url": "https://mcp.notion.com/mcp"
}
}
}
```
```json SSE theme={null}
{
"mcpServers": {
"notion": {
"type": "sse",
"url": "https://mcp.notion.com/sse"
}
}
}
```
```json STDIO (via mcp-remote) theme={null}
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"]
}
}
}
```
Use the STDIO configuration if your client doesn't support remote HTTP connections.
## Connect through the Notion app
You can also start the connection from Notion:
Open **Settings** in the Notion app
Go to **Connections** → **Notion MCP**
Choose your MCP client from the list and complete the OAuth flow
## Troubleshooting
Some MCP clients only support local stdio servers. You can still connect to Notion MCP using the [mcp-remote](https://www.npmjs.com/package/mcp-remote) bridge:
```json theme={null}
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"]
}
}
}
```
As a last resort, you can run our [open-source MCP server](https://github.com/makenotion/notion-mcp-server) locally, though this package is no longer actively maintained.
* Complete the OAuth flow when prompted.
* Disconnect and reconnect the client. Look for **Clear authentication** or **Disconnect** in its MCP settings.
* Check your permissions in the Notion workspace that you're trying to access.
Check the client's documentation for how to add a remote MCP server. Most MCP clients accept a URL or JSON configuration. If the client doesn't support MCP, contact its developer.
## FAQ
Yes. Notion MCP accepts a [personal access token (PAT)](/guides/get-started/personal-access-tokens) from MCP clients that can send custom HTTP headers. Connect to `https://mcp.notion.com/mcp` and send the PAT in the authorization header:
```http theme={null}
Authorization: Bearer {PERSONAL_ACCESS_TOKEN}
```
The PAT must have the **Notion API** capability. It uses the permissions of the person who created it, so store it in an environment variable or secret manager and don't put it in shared client configuration.
Not every MCP client supports custom HTTP headers. PAT-authenticated requests also don't appear in [List MCP client connections](/reference/admin/list-mcp-client-connections). Organization owners can manage these credentials with [List personal access tokens](/reference/admin/list-personal-access-tokens) and [Revoke a personal access token](/reference/admin/revoke-personal-access-token).
Review [security best practices](/guides/mcp/mcp-security-best-practices) before allowing an MCP client to take actions without confirmation.
Yes. Use the [`notion-create-file-upload` tool](/guides/mcp/mcp-supported-tools#create-a-file-upload-url) to upload images and other files up to 20 MiB. Workspace file-size limits still apply.
The tool returns a short-lived upload URL and the headers and form field that the MCP client must use to send the file. After the upload succeeds, pass the response's `suggested_markdown` directly to `notion-create-pages` or `notion-update-page`, or include it on a separate line in `notion-create-comment` markdown to attach the file. For larger files, use the [file upload API](/guides/data-apis/working-with-files-and-media).
**Notion MCP** (`https://mcp.notion.com/mcp`) is our hosted, actively maintained server. It supports OAuth and PAT bearer authentication and requires no infrastructure setup.
The **open-source server** ([`notion-mcp-server`](https://github.com/makenotion/notion-mcp-server)) is no longer actively maintained. It supports bearer token authentication and the original JSON-based v1 APIs, which may be useful for automated workflows, but requires you to manage your own connection and deployment.
For most users, we recommend Notion MCP.
If you're integrating Notion MCP into your own application, see
[Build an MCP client for Notion](/guides/mcp/build-mcp-client) for OAuth
and connection requirements.
## Next steps
See the [Notion MCP tools](/guides/mcp/mcp-supported-tools) that a connected client can use.
# Hosting a local MCP server
Source: https://developers.notion.com/guides/mcp/hosting-open-source-mcp
Learn how and when to use the open-source Notion MCP server.
The open-source `notion-mcp-server` package is no longer actively maintained. We recommend the remote [Notion MCP](/guides/mcp/get-started-with-mcp) server for most clients. Issues and pull requests on the open-source repository are not actively monitored.
If you need to run your own MCP server, the [`notion-mcp-server` package](https://github.com/makenotion/notion-mcp-server) is available as an open-source implementation. This option may be suitable if you:
* Need bearer token authentication for fully automated workflows
* Already have an existing Notion connection you want to reuse
* Require access to the original JSON-based v1 APIs
* Prefer to manage infrastructure yourself
Compared to Notion's remote MCP, running the open-source server requires more technical setup, including provisioning your own Notion connection, managing API tokens, and handling deployment.
The remote Notion MCP server at `https://mcp.notion.com/mcp` requires no self-hosted infrastructure and receives current tools and updates.
## Next steps
If you still want to self-host, explore the [GitHub repo](https://github.com/makenotion/notion-mcp-server) to get started.
For the recommended approach, see [Connecting to Notion MCP](/guides/mcp/get-started-with-mcp).
# Security best practices
Source: https://developers.notion.com/guides/mcp/mcp-security-best-practices
Learn how to keep your workspace secure when using Notion MCP.
Use Notion's official MCP endpoints:
1. [https://mcp.notion.com/mcp](https://mcp.notion.com/mcp) for Streamable HTTP (recommended).
2. [https://mcp.notion.com/sse](https://mcp.notion.com/sse) for Server-Sent Events (SSE).
Review the [documentation for common MCP clients](/guides/mcp/common-mcp-clients), and only connect clients you trust. A connected client can use Notion MCP to access content that you can access. Before installing a client from a third-party marketplace, verify the marketplace and the MCP server URL.
## Protect against prompt injection
[Prompt injection](https://devblogs.microsoft.com/blog/protecting-against-indirect-injection-attacks-mcp) occurs when untrusted content contains instructions that try to change a client's behavior.
**Protect your data**
An attacker could put a malicious instruction in content that your MCP client reads. If the client follows the instruction, it could disclose data or change content without your intent.
Treat content returned by tools as untrusted. Review proposed actions and data sharing before approving them.
Review the permissions of every MCP client and tool. Notion MCP operates within your workspace, but a connected client may send content returned by Notion MCP to systems outside Notion.
Enable confirmation before actions that change content. This lets you review an action before the MCP client runs it.
## Admin controls
Workspace owners can manage MCP client access in **Settings** → **Connections**. Organization owners can also use the Admin API to [list](/reference/admin/list-mcp-client-connections) and [revoke](/reference/admin/revoke-mcp-client-connection) members' connections. See [Admin controls for MCP](https://www.notion.com/help/notion-mcp#admin-controls-for-mcp) for details.
# Supported tools
Source: https://developers.notion.com/guides/mcp/mcp-supported-tools
Learn what you can do with Notion MCP tools.
Notion MCP provides tools for searching, reading, and changing content in your Notion workspace.
An MCP client can call several tools in one task. For example, it can search for pages, create a page from the results, and then update page properties.
## MCP tools
`notion-search`
Search across your Notion workspace and connected tools like Slack, Google Drive, and Jira.
Requires Notion AI access. Without a Notion AI plan, search is limited to your Notion workspace only.
**Example prompts:**
* "Check Slack for how we solved this bug in the past"
* "Search for documents mentioning 'budget approval process'"
* "Look for meeting notes from last week with John"
* "Find all project pages that mention 'ready for dev'"
`notion-fetch`
Retrieves content from a Notion page, database, or data source by its URL or ID. You can pass a data source ID (from `collection://...` tags in database responses) to fetch details about that specific data source, including its schema and properties. When fetching a database, the response includes available templates for each data source, which can be used with the create-pages and update-page tools.
Pass the special id `self` to retrieve the connected workspace and user identity instead of an entity. The response includes a `self` object with the workspace's ID and name, and the authenticated user's ID, name, type, and email — useful for labeling a connection after OAuth.
The `self` object also includes `current_tool_access`, a map of tool names to their access state on this workspace's plan: `available`, `available_with_limit` (calls can be made up to the limit included with the workspace's plan), `upgrade_required` (calls return an upgrade prompt, and the map entry carries an `upgrade_url`), or `not_enabled`. Tools are listed on every plan, so consult this map to route away from tools that would only return an upgrade prompt. For `query_data_sources`, the status describes single-data-source SQL access; view mode is always available, while queries across multiple data sources still require Enterprise with Notion AI. Keys are the tools' base names; when tools appear with a `notion-` prefix and hyphens (e.g. `notion-query-data-sources`), they correspond to the map key with the prefix dropped and hyphens as underscores (`query_data_sources`).
When a page is large enough that some subtrees could not be loaded, the response sets `truncated` to `true` and includes `unknown_block_ids` (up to 50 omitted subtree root IDs) and `unknown_block_count` (the total number of omitted subtree roots). Pass one of the returned IDs back to `notion-fetch` to retrieve that subtree directly. An ID may also represent content the caller cannot access, so treat an `object_not_found` error on retry as a permissions signal rather than a failure to handle.
**Example prompts:**
* "What product requirements still need to be implemented from this ticket `https://notion.com/page-url`?"
* "Fetch the data source `collection://f336d0bc-b841-465b-8045-024475c079dd` to see its schema"
* "Fetch the bug tracking database so I can see the available templates"
* "Fetch `self` to see which workspace and user this connection is for"
`notion-create-file-upload`
Creates a short-lived URL for uploading one local file directly to Notion. After calling the tool, the MCP client sends the file as a `multipart/form-data` POST request using the returned URL, headers, and form field. The upload response includes `suggested_markdown`, which can be passed directly to `notion-create-pages` or `notion-update-page`, or included on a separate line in `notion-create-comment` markdown to attach the file.
Files are limited to 20 MiB for this single-part upload flow, and workspace file-size limits still apply. For larger files, use the [file upload API](/guides/data-apis/working-with-files-and-media).
**Example prompts:**
* "Upload `diagram.png` and add it to the project plan"
* "Attach `report.pdf` to a new page"
* "Upload this file and include it in my comment"
`notion-create-attachment`
Creates a Notion attachment from exactly one source: inline UTF-8 text, a file at a direct public HTTPS URL, or a completed file upload created by the same integration. The result includes `suggested_markdown`, which can be passed directly to `notion-create-pages` or `notion-update-page`, or included on a separate line in `notion-create-comment` markdown to attach the file.
Inline content supports text formats such as HTML, Markdown, CSV, JSON, and SVG, up to 200 KiB. URL downloads support binary files, must complete within one minute, and are limited to 5 MiB on free workspaces or 50 MiB on paid workspaces. URLs must not redirect, require request headers or cookies, or resolve to a private network address. For local files, use `notion-create-file-upload` when available. For files that exceed these limits or downloads that require redirects or authentication, use the [file upload API](/guides/data-apis/working-with-files-and-media) and pass the resulting upload ID as `source_file_id`.
**Example prompts:**
* "Create an HTML attachment from this report and add it to the project page"
* "Attach the PDF at this direct download URL to my meeting notes"
* "Add the file I just uploaded to a comment"
`notion-download-attachment`
Downloads the complete UTF-8 text content of an attachment created by `notion-create-attachment`. Pass the `file_upload_id` returned when the attachment was created. The attachment must belong to the same integration, have completed uploading, and use a supported text format such as HTML, Markdown, plain text, CSV, JSON, XML, CSS, YAML, TSV, calendar, GPX, or SVG.
Downloads are limited to 200 KiB. This tool does not fetch arbitrary URLs or return binary files. For larger or binary attachments, use the signed file URL returned when reading the page that contains the attachment.
**Example prompts:**
* "Download the HTML attachment I just created so I can edit it"
* "Read the contents of this Markdown attachment"
* "Retrieve the text attachment with this file upload ID"
`notion-create-pages`
Creates one or more Notion pages with specified properties and content. Supports applying [database templates](/guides/data-apis/creating-pages-from-templates) to pre-populate new pages with content and property values. Each page can optionally have an icon (emoji, custom emoji by name, or external URL) and a cover image. If a parent is not specified, a private page will be created.
**Example prompts:**
* "Create a project kickoff page under our Projects folder with agenda and team info"
* "Make a new employee onboarding checklist in our HR database"
* "Create a new bug report in the tracking database using the 'Urgent Bug' template"
* "Add a new product feature request to our feature database"
* "Create a page with the 🚀 icon and a cover image"
`notion-update-page`
Update a Notion page's properties, content, icon, or cover. Supports applying [database templates](/guides/data-apis/creating-pages-from-templates) to existing pages. Icon and cover can be set alongside any update command.
**Example prompts:**
* "Change the status of this task from 'In Progress' to 'Complete'"
* "Add a new section about risks to the project plan page"
* "Apply the project kickoff template to this page"
* "Set the page icon to 🎯 and add a cover image"
* "Remove the icon from this page"
`notion-convert-page-to-skill`
Marks a Notion page as an AI skill. Pass the page's full Notion URL. The page must be in the connected workspace, and you must have permission to edit it.
**Example prompts:**
* "Convert this page into a skill: `https://www.notion.so/example-page-url`"
* "Make our engineering guidelines page available as an AI skill"
`notion-move-pages`
Move one or more Notion pages or databases to a new parent.
**Example prompts:**
* "Move my weekly meeting notes page to the 'Team Meetings' page"
* "Reorganize all project documents under the 'Active Projects' section"
`notion-duplicate-page`
Duplicate a Notion page within your workspace. This action completes asynchronously.
**Example prompts:**
* "Duplicate my project template page so I can use it for the new Q3 initiative"
* "Make a copy of the meeting agenda template for next week's planning session"
`notion-create-database`
Creates a new Notion database, initial data source, and initial view with the specified properties.
**Example prompts:**
* "Create a new database to track our customer feedback with fields for customer name, feedback type, priority, and status"
* "Set up a content calendar database with columns for publish date, content type, and approval status"
`notion-create-folder`
Create an empty Folder under a parent page.
**Example prompts:**
* "Create a folder named 'Project files' under this page"
* "Create a folder named 'Supporting documents' under this project page"
`notion-update-data-source`
Update a Notion data source's properties, name, description, or other attributes.
**Example prompts:**
* "Add a status field to track project completion"
* "Update the task database to include priority levels"
`notion-create-view`
Create a new view on a Notion database. Supports table, board, list, calendar, timeline, gallery, form, chart, map, and dashboard view types. Use the optional configuration DSL for filters, sorts, grouping, and display options.
**Example prompts:**
* "Create a board view grouped by Status in my tasks database"
* "Add a calendar view to the project tracker that shows items by due date"
* "Set up a filtered table view that only shows in-progress items, sorted by priority"
* "Create a timeline view for the roadmap database using start and end dates"
* "Create a chart view showing task counts by status as a bar chart"
* "Add a form view to the feedback database for collecting responses"
* "Create a map view of office locations using the Address property"
`notion-update-view`
Update a view's name, filters, sorts, or display configuration. Only the fields you specify will be changed. Supports clearing existing configuration like filters, sorts, and grouping.
**Example prompts:**
* "Rename the 'All Tasks' view to 'Sprint Board'"
* "Update the board view to filter by status equals 'Done'"
* "Clear the filters on this view and add a sort by created date"
* "Change the view to group by priority and only show Name and Status columns"
`notion-query-data-sources`
Query Notion data sources with SQL, or run an existing view, with structured summaries, grouping, and filters. Returns organized results with counts and rollups for quick scanning.
View mode is available on every plan without a tool-specific quota. Single-data-source SQL is unlimited on Business and Enterprise plans with Notion AI; other plans share a per-workspace usage limit and receive an upgrade prompt after the allowance is exhausted. Queries across multiple data sources require an Enterprise plan with Notion AI.
**Example prompts:**
* "What's due for me this week across all tasks and meeting note action items? Group by priority."
* "Show all risks from Engineering and Product databases this month, grouped by owner."
`notion-query-database-view`
Query data from a Notion database using a pre-defined [view's filters and sorts](https://www.notion.com/help/views-filters-and-sorts).
Available on every plan without a tool-specific quota.
**Example prompts:**
* "Query my 'In Progress' tasks view to see what I'm currently working on"
* "Get all items from the 'High Priority' view in our feature requests database"
* "Export the filtered data from the 'Q1 Goals' view for analysis"
`notion-query-meeting-notes`
Query the current user's meeting notes, filtering by meeting-specific properties (such as a title keyword search). Returns meetings where the user is an attendee or creator by default.
Available on all plans, but using it requires a Business plan or higher with Notion AI. On other plans the tool returns an upgrade prompt.
**Example prompts:**
* "Find my meeting notes from this week"
* "What were the action items from my sprint planning meetings?"
* "Show my 1:1 meeting notes with Alice"
`notion-create-comment`
Add a comment to a page or specific content. Supports page-level comments,
block-level comments (via content selection), and replies to existing discussions.
**Example prompts:**
* "Add a feedback comment to this design proposal"
* "Comment on the 'Budget' section of the quarterly review"
* "Reply to the discussion about deadline concerns"
* "Leave a note on the meeting notes about the action items"
`notion-get-comments`
Lists all comments and discussions on a page. Can include block-level and
inline discussions, resolved threads, and full comment content.
**Example prompts:**
* "Get all discussions on this page, including resolved ones"
* "Show me the comments on the Requirements section"
* "Get all feedback comments from last week's review"
`notion-get-teams`
Retrieves a list of teams (teamspaces) in the current workspace.
**Example prompts:**
* "Search for teams by name, and your membership status in each team"
* "Get a team's ID to use as a filter for a search"
`notion-get-users`
Lists workspace members and guests with their IDs, names, emails (when available), and types (person or bot). Supports pagination and search by name or email. You can also retrieve a specific user by ID, or the current user (or bot) by passing `self`.
**Example prompts:**
* "Get contact details for the user who created this page"
* "Look up the profile of the person assigned to this task"
* "Find users whose name or email matches 'john'"
* "What's my Notion user ID and email?"
`notion-get-async-task`
Retrieves the current status of an async task started by another tool (such as `notion-duplicate-page`, or `notion-create-pages` when invoked with `allow_async: true`). Returns one of `queued`, `running`, `retrying`, `succeeded`, or `failed`. When a task has succeeded, the operation's result is included in the response; when it has failed, an error is included instead.
Poll this tool with the `task_id` from the originating tool's `async_task` response. Wait briefly between polls — the original response includes a suggested backoff.
**Example prompts:**
* "Check whether the page I just duplicated is ready"
* "Poll this async task until it completes and show me the result"
## Async page create and update
The `notion-create-pages` and `notion-update-page` tools support `allow_async: true` for long-running page create and update work. When `allow_async` is omitted or `false`, the tools keep their normal synchronous result shape. `allow_async` changes response behavior only; the same validation, permissions, and write operation still apply.
Use async mode when asking an assistant to create or update a page with a large amount of markdown content. The initial tool call returns an `async_task` handle, and the assistant can poll `notion-get-async-task` with the returned `async_task.id` passed as `task_id` until the task reaches `succeeded` or `failed`.
```json theme={null}
{
"tool": "notion-create-pages",
"arguments": {
"allow_async": true,
"parent": { "page_id": "YOUR_PAGE_ID" },
"pages": [
{
"properties": { "title": "Migration plan" },
"content": "# Migration plan\n\nLarge markdown content..."
}
]
}
}
```
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "queued",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"poll_after_seconds": 2,
"operation": {
"surface": "mcp",
"name": "create_pages"
}
}
```
```json theme={null}
{
"tool": "notion-update-page",
"arguments": {
"allow_async": true,
"page_id": "YOUR_PAGE_ID",
"command": "replace_content",
"new_str": "# Updated plan\n\nLarge replacement markdown..."
}
}
```
```json theme={null}
{
"tool": "notion-get-async-task",
"arguments": {
"task_id": "task_abc123"
}
}
```
If the task is still `queued`, `running`, or `retrying`, wait at least the suggested `poll_after_seconds` before polling again. A `succeeded` task includes the operation result. A `failed` task includes an error object that the assistant can summarize or use to retry with a corrected request.
**Tool names may vary for OpenAI**
When connecting with an OpenAI MCP client (e.g. ChatGPT), the `notion-` prefix is automatically omitted from the `notion-fetch` and `notion-search` tools, making them appear as `fetch` and `search`, respectively. This is because these specific tool names are required as part of the [Deep Research specification](https://platform.openai.com/docs/guides/deep-research#remote-mcp-servers) for remote MCP servers.
## Rate limits
Standard [API request limits](/reference/request-limits) apply per user's usage of Notion MCP (totaled across all tool calls). Currently, this is an average of **180 requests per minute** (3 requests per second). A separate limit applies per workspace, shared across all of its connections and scaled to the workspace's plan. Because it's shared, you can be rate limited even when you're under the per-user limit above.
Some MCP tools have additional, tool-specific rate limits that are stricter. These are subject to change over time, but the current values are listed below for reference:
* **Search**: 30 requests per minute
### Examples
To illustrate the above limitations, you'll experience rate limit errors in your MCP client of choice in any of the following example scenarios (assuming we take the average rate over a large enough time window):
* 35 searches per minute (exceeds search-specific limit)
* 12 searches & 170 fetches per minute (exceeds general 180 requests/min limit)
* 185 fetches per minute (exceeds general 180 requests/min limit)
### What to do if you're rate-limited
In most cases, the time it takes to do a complex AI-powered search across Notion and your connected tools means that sequential searches will typically stay under the rate limit. In general, if you encounter rate limit errors, prompt your LLM tool to reduce the amount of parallel searches or operations performed using Notion MCP, and/or try again later.
# Notion MCP
Source: https://developers.notion.com/guides/mcp/overview
Learn how MCP clients connect to your Notion workspace.
Notion MCP lets an [MCP client](/guides/mcp/common-mcp-clients), such as Claude Code, Cursor, or Codex, connect to your Notion workspace through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction).
## How Notion MCP works
Notion MCP is a remote MCP server hosted by Notion. After you authorize a connection with OAuth, the MCP client can use Notion MCP tools to read and update content that you can access.
## What you can do
MCP clients can combine Notion MCP tools to:
* Search content in Notion and connected sources.
* Read, create, and update Notion content.
* Create pages and databases.
See [Supported tools](/guides/mcp/mcp-supported-tools) for the current tool list, or [connect an MCP client](/guides/mcp/get-started-with-mcp).
## Admin controls
Workspace owners can manage MCP client access in **Settings** → **Connections**. Organization owners can also use the Admin API to [list](/reference/admin/list-mcp-client-connections) and [revoke](/reference/admin/revoke-mcp-client-connection) members' connections.
See [Admin controls for MCP](https://www.notion.com/help/notion-mcp#admin-controls-for-mcp) in the Notion Help Center for workspace settings and allowlists.
# Example code
Source: https://developers.notion.com/guides/resources/examples
# Historical changelog
Source: https://developers.notion.com/guides/resources/historical-changelog
View an archive of Notion Developers updates prior to September 2023
**View the current [Changelog](/page/changelog) for the newest updates.**
This page is a historical archive of updates older than September 2023. This is kept separate from the current changelog to keep page navigation faster while retaining older updates.
* The [Working with databases guide](/guides/data-apis/working-with-databases) was revised to improve its readability and to make additional resources easier to find.
* Notion's [Postman collection](https://www.postman.com/notionhq/notion-s-api-workspace/collection/52041987-03f70d8f-b6e5-4306-805c-f95f7cdf05b9) for the API was updated. Be sure to pull recent changes into your forked version.
* A reminder was added to the [Comments endpoints](/reference/create-a-comment) to update [integration capabilities](/reference/capabilities) for comments prior to using the endpoints. (Read/write comment capabilities are off by default and can be turned on in the integration dashboard.)
* General clean-up and improvements, including code formatting.
* The [Build your first integration](/guides/get-started/quick-start) guide was rewritten with new demo code to help developers learn how to use Notion’s API even faster.
* A new [sample app](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/web-form-with-express) was added to the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js) `/examples` directory. This completed sample app is referenced in the new [Build your first integration](/guides/get-started/quick-start) guide.
* The description for the `block_id` path parameter was updated for the [Append block children](/reference/patch-block-children) endpoint to indicate that a block ID or page ID can be used.
* A clarification was added to [documentation](/reference/retrieve-a-database) for retrieving/updating database properties: If a property is based on a relation to another database, the related database also needs to be shared with the integration.
* A clarification was added to documentation for [querying databases](/reference/post-database-query-filter#multi-select). When filtering a multi-select property, the `contains` field will filter for exact matches for the string provided.
* The [Working with comments](/guides/data-apis/working-with-comments) guide was updated with additional examples to distinguish between creating page comments and inline discussion comments.
* The [Create a comment](/reference/create-a-comment) endpoint description now links to the Working with comments guide to help developers find additional resources faster.
* If you haven’t already, join our [Notion Devs Slack group](https://join.slack.com/t/notiondevs/shared_invite/zt-3u9oid9q8-HLUBmMVWYK~g9HFo4U4raA) to learn from other developers building with the public API.
### Notice for an upcoming Public API change
#### We will soon be rolling out changes to the Formulas property (Formulas 2.0), and as such, we will be making a change to the Notion Public API.
This is a non-versioned change and is expected to be in effect in the next couple weeks.
**tl;dr:** As part of the Formulas 2.0 rollout, the Public API’s format of the string value for [`formula.expression`](/reference/property-object#formula) will be changing. Public API calls with formula inputs in the old format will still succeed. On write operations, the old format will be supported indefinitely, but on read, only the new format will be returned. This change is being made to improve the formulas experience and ensure parity with the Notion app.
No action is required for creating or updating database formulas. Reading database [`formula.expression`](/reference/property-object#formula) values may require developer changes.
#### What do you need to know?
**The string value of `formula.expression` is changing; the schema is not.**
* **On write** via the Public API ([create](/reference/create-a-database) or [update](/reference/update-a-database) database endpoints), Notion will support using the old format as a Public API input in the formula property schema indefinitely *and* will support writing in the **new** format.
* Database objects returned via the Public API will have the new formulas 2.0 format.
```javascript javascript expandable theme={null}
// Old format
"Updated price": {
"id": "YU%7C%40",
"name": "Updated price",
"type": "formula",
"formula": {
"expression": "prop(\"Price\") * 2"
}
}
// New format (upcoming)
"Updated price": {
"id": "YU%7C%40",
"name": "Updated price",
"type": "formula",
"formula": {
"expression": "{{notion:block_property:BtVS:00000000-0000-0000-0000-000000000000:8994905a-074a-415f-9bcf-d1f8b4fa38e4}}/2"
}
}
```
#### Why is this happening?
Notion databases allow you to build a fully customizable system for you and your team – they provide a place where you can keep all your information in one place, with the ability to build views, filters, and workflows that can be adapted to your needs.
The formula property helps you take that even further – allowing you to perform calculations, create specialized views, and provide an extra layer of insight based on information in other database properties. It helps expand what you can do in Notion databases.
We are improving the formulas experience so that:
1. It’s easier to write formulas.
2. Formula outputs look and feel more native to Notion.
3. The formula language can fulfill more specific needs.
Changes being made to the API are to ensure parity with the Notion app.
#### What do you need to do?
This is a non-versioned change that will not affect most developers. As mentioned, the formula property format will still have the same schema in the Public API; only the value of the `formula.expression` field will change.
Keep an eye on this changelog for when the update becomes available in the Public API.
* Notion is excited to announce our [Technology Partnership Program](https://www.notion.so/technology-partner-program). 🎉 This program is open to companies who have built a public integration and are interested in improving and scaling their integration with Notion’s support. If you think your integration and company could be a fit, [learn more and apply here](https://www.notion.so/technology-partner-program).
* We’ve updated our API reference docs to include information on Notion’s [wiki databases and verified pages](https://www.notion.so/help/wikis-and-verified-pages). Updates include:
* An overview on wikis in the guide to [working with databases](/guides/data-apis/working-with-databases#database-properties).
* The [`verification`](/reference/page-property-values#verification) page property was added to the [Page properties](/reference/page-property-values) documentation.
* The [Create a database](/reference/create-a-database) and [Query a database](/reference/post-database-query) endpoint documentation was updated to reflect API changes related to wikis. Namely, that querying wiki databases can return both [Page](/reference/page) and [Database](/reference/database) objects.
* The [Error codes](/reference/status-codes#error-codes) section in the [Status code](/reference/status-codes) page was updated to include examples of the `"message"` returned with each type of API error, as well as descriptions of the issue each error code represents.
* A number of sample cURL commands in our docs were still using an old [Notion Version](/reference/versioning) in their headers. These have all been updated.
* A clarification was added to the [Authorization guide](/guides/get-started/authorization#making-api-requests-with-an-internal-connection) that the [Notion Version](/reference/versioning) is always required in public API request headers.
* A new integration [example](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/parse-text-from-any-block-type) was added to the Notion SDK for JavaScript repo. This example shows how to get the plain text from any block type currently supported by the public API.
* The new unique ID page property was added to the [Page properties](/reference/page-property-values) documentation. When used, the unique ID (`unique_id`) auto-increments for every new page created in a database. An optional prefix can be included that will be applied to the ID values.
* Workspace Owners can now see *all* internal integrations created in a workspace via the integration dashboard. This includes integrations created by themselves and other Workspace Owners. We’ve included this information in our [Getting Started](/guides/get-started/overview#connection-types) guide.
* A [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js) code sample was added to the [Create a database](/reference/create-a-database) endpoint documentation.
* We updated our [Getting started guide](/guides/get-started/overview) to help developers who are new to the public API better understand how the API relates to integrations.
* The [Block object](/reference/block#embed) docs were updated with a tip on how to embed Vimeo links in a Notion page via the API.
* A new `after` parameter has been added to the [Append block children](/reference/patch-block-children) endpoint. Developers can now specific where to append a new block, instead of appending it to the end of a parent block by default.
```bash cURL theme={null}
curl -X PATCH https://api.notion.com/v1/blocks/16d8004e-5f6a-42a6-9811-51c22ddada12/children \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2022-06-28" \
--data '{
"children": [
...
}
], after: ""
}'
```
* The [Authorization guide](/guides/get-started/authorization) had a clarification added to help developers find the resources they need for Link Preview integrations.
* The new `public_url` property was added to the docs. When a page or database has been shared publicly, the response body will include a `public_url` value.
```json JSON theme={null}
{
"object": "page",
"id": "",
"created_time": "2023-06-02T19:54:00.000Z",
"last_edited_time": "2023-06-02T23:04:00.000Z",
"created_by": { ... },
"last_edited_by": { ... },
"cover": null,
"icon": null,
"parent": { ... },
"archived": false,
"properties": { ... },
"url": "",
"public_url": ""
}
```
* The [Retrieve block children](/reference/get-block-children) endpoint documentation was updated to help developers who are new to the public API better understand the endpoint’s functionality.
* The [Retrieve a block](/reference/retrieve-a-block) endpoint documentation was updated with some additional information related to working with page content.
* The `invalid_grant` code was added to our [Status codes](/reference/status-codes) documentation. This code is returned when the authorization grant (e.g. token) provided is invalid. For example, a status code `400` with an `invalid_grant` code will be returned when the token provided has expired.
* The [Rich text](/reference/rich-text) documentation was updated with additional information on what rich text is and how the Notion uses it.
* Our guides and docs related to Link Preview integrations have been updated to help developers find the information they need faster. Improvements have been made to the following guides and API reference docs:
* [Getting started guide](/guides/get-started/overview)
* [Introduction to Link Previews guide](/guides/link-previews/introduction)
* Link Preview integration guide
* [Unfurl attribute object docs](/reference/unfurl-attribute-object)
* We added more information about the `plain_text` property found in the `rich_text` object. Learn more about rich text in our [Rich text object](/reference/rich-text) docs.
* The docs related to [filtering](/reference/post-database-query-filter) and [sorting](/reference/post-database-query-sort) database queries now have more code examples for developers building integrations with the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js).
* We reorganized the REST API reference navigation bar after removing the “Other” section to make its child pages easier to find.
* The [Query a database](/reference/post-database-query) and [Filter database entries](/reference/post-database-query-filter) docs were updated with additional code examples of passing single and multiple filters.
* The [Working with comments](/guides/data-apis/working-with-comments) guide was updated to clarify how to retrieve and add comments using the REST API.
* The references docs for `rollup` [page properties](/reference/page-property-values#rollup), as well as the [Retrieve a page property](/reference/retrieve-a-page-property) and [Retrieve a page](/reference/retrieve-a-page) endpoints were updated with additional information related to limitations. In cases where a page property — like a rollup — has more than 25 references, the [Retrieve a page property](/reference/retrieve-a-page-property) endpoint must be used to receive a complete response.
* An outdated Changelog URL now redirects to this Changelog page to help avoid confusion.
* We added a database schema size recommendation of **50KB** to our docs to help developers manage their database query performance. It is strongly recommended that developers keep their schema size under this number.
* The [Update a database](/reference/update-a-database) page was updated to improve readability. Additional information on how this endpoint differs from related endpoints was also added to help developers better navigate the REST API docs.
* The [Query a database](/reference/post-database-query#errors) page was updated with additional information about the `filter_properties` query parameter. When used with the REST API, this query parameter is passed as a string, like so:
```
https://api.notion.com/v1/databases/[database_id]/query?filter_properties=[property_id_1]&filter_properties=[property_id_2]
```
When used with the [JavaScript SDK](https://github.com/makenotion/notion-sdk-js), the `filter_properties` option accepts an array of property ID strings:
```javascript JavaScript theme={null}
notion.databases.query({
database_id: 'databaseID',
filter_properties: ["propertyID1", "propertyID2"]
})
```
* Docs that mention the `redirect_uri` — a value used with [public integrations](/guides/get-started/public-integrations) — were updated to clarify when this value is required. Refer to the [Create a token](/reference/create-a-token) page for a complete description.
* The video block-type was updated on the [Block Object](/reference/block#video) page to clarify accepted video types. YouTube URLs that contain `watch` or `embed` are supported video types.
* The [Append a block](/reference/patch-block-children) page content was reorganized to improve readability.
* Link Preview integration docs were updated to reflect a change regarding how link previews are enabled in the integration dashboard.
* The [versioning page](/reference/versioning) was updated to clarify that the `Notion-Version` header is required in Notion REST API requests.
* The [parent object page](/reference/parent-object) and API reference docs for [database POST requests](/reference/create-a-database) and [blocks PATCH requests](/reference/patch-block-children) were updated to better explain how parenting rules work.
* The [Integration guide](/guides/get-started/quick-start) was updated with more links to help developers find resources faster.
* Number database properties now support the Peruvian sol as a currency format. To use it, set `"peruvian_sol"` as the value for a number’s `format` field when creating or updating a database property or [schema](/reference/property-schema-object#number-configuration).
* General docs housekeeping, such reducing the number of callouts in our API reference docs to improve the readability.
* Our developer community Slack invite link was updated. [Join here](https://join.slack.com/t/notiondevs/shared_invite/zt-3u9oid9q8-HLUBmMVWYK~g9HFo4U4raA) to connect with other developers building with the Notion API.
* The [Authorization guide](/guides/get-started/authorization) was updated to include more information on creating integrations, adding templates to public integrations, and more code examples to get you started, faster.
* We’ve added more code examples to our API reference docs, including [Archive a page](/reference/archive-a-page) and [Authentication](/reference/authentication).
* General docs housekeeping, such reducing the number of callouts in our API reference docs to improve the readability.
We don’t have any changes to announce this week! Stay tuned, and in the meantime check out our platform roadmap for a look at what we’re building.
### Fixes and improvements
* You can now update [rollup database properties](/reference/property-object#rollup) via the API. To programmatically update a `rollup` property, send a PATCH to [Update a database](/reference/update-a-database) that specifies the change in the `properties` body param.
We don’t have any updates to share right now. Stay tuned for the next changelog! To get a sense for what we’re heads down working on, check out the [platform roadmap](/page/changelog).
### Fixes and improvements
* Stay tuned!
### New things
* Added a token `Refresh` button to the settings page for internal integrations. Click `Refresh` to generate a new token for your internal integration.
### New things
* Shipped detailed docs for Link Previews including an [overview](/guides/link-previews/introduction) and [reference materials](/reference/unfurl-attribute-object).
### Fixes and improvements
* The [Retrieve a Page endpoint](/reference/retrieve-a-page#errors) can now return specific page property values when you include the `filter_properties` path param.
* You can now request specific page property values from a database by passing `filter_properties` in the request body to the [Query a database endpoint](/reference/post-database-query).
### New things
* Happy 2023! For a sneak peek of what we’ll be up to this year, check out our updated [platform roadmap](/page/changelog).
### Fixes and improvements
* Updated the [Append block children](/reference/patch-block-children) and [Retrieve block children](/reference/get-block-children) endpoints to specific supported block types to create a more consistent dev experience. The endpoints now throw an error if the block type in the request does not [support children](/reference/block#block-types-that-support-child-blocks).
### New things
* Built a ✨Glitch ✨ demo that updates Notion tasks when a linked GitHub PR is closed or merged. [Give it a spin!](https://glitch.com/~notion-task-github-pr-sync)
We took advantage of the US Thanksgiving holiday to host a mini internal hackathon.
Nothing to share from that, yet! It’s been a quiet few weeks.
If you want something to read while you stay tuned for the next update, check out the revised [Get started](/guides/get-started/overview) guide.
* We added a `this_week` filter for database queries. You can now search for database entries where the `"date"`, `"created_time"`, or `"last_edited_time"` property value falls within the current week. Refer to the [date filter condition](/reference/post-database-query-filter#date) docs for details.
* Number database properties now support the Singapore dollar as a currency format. To use it, set `"singapore_dollar"` as the value for a number’s `format` field when creating or updating a database [property](/reference/property-object#number) or [schema](/reference/property-schema-object#number-configuration).
* You can now add a Notion template option to a public integration from the integration's settings page. For details on what the permissions flow looks like for users who opt in to the template, refer to the [Authorization guide](/guides/get-started/authorization).
* A [`relation`](/reference/property-value-object#relation-property-values) property value now includes a `has_more` property when returned by the [Retrieve a page endpoint](/reference/retrieve-a-page). `has_more` is `true` if the `relation` has more than 25 page references. Otherwise, `has_more` is `false`.
* We added a `workspace_name` property to [bot user objects](/reference/user#bots). If the bot `owner.type` is `"workspace"`, then `workspace.name` identifies the name of the workspace that owns the bot. If the `owner.type` is `"user"`, then `workspace.name` is `null`.
Started an experiment to improve [search endpoint](/reference/post-search) performance by tweaking how we call Elasticsearch under the hood.
* Fixed a bug where date mentions ended in a `→` character even if they only represented a single date, not a date range.
* Added an `Authorization URL` field to the public integration form. You can now click to copy the URL that allows users to authorize your integration (read more in the [Authorization guide](/guides/get-started/authorization)).
* Corrected an error that caused the [`getProperty` endpoint](/reference/retrieve-a-page-property) to return only one item if the `property_id` belonged to a multi-item [`Files`](/reference/property-item-object#files) page property.
### Version 2022-06-28 includes page property types and values
Responses for page retrievals, database queries, and searches will again include page property types and values. This matches the behavior in version `2022-02-22` and takes effect on August 31, 2022.
### Features
* The public API now supports the following functionality for `status` properties:
* Reading and updating `status` properties on pages ([read more](/reference/property-value-object#status-property-values))
* Reading, but not updating, `status` property configuration on databases ([read more](/reference/property-object#status))
* Filtering or sorting by `status` properties when querying databases ([read more](/reference/post-database-query-filter#status))
* `header_1`, `header_2`, and `header_3` blocks now have an `is_toggleable` property, to better indicate whether they are heading toggle blocks. ([read more](/reference/block#headings))
* Headings can be togglified and un-togglified by setting `is_toggleable` to true or false, but note that all the children inside the toggle must be removed before it can be untogglified.
No updates for these past two weeks, but stay tuned for the next changelog!
### Features
* Added support for [reading](/reference/list-comments) and [writing](/reference/create-a-comment) page-level comments in the API.
### Comments API
Today we're launching a brand new set of APIs for interacting with Notion comments. This includes the ability to:
* Read comments from a page or block.
* Add a comment to a page.
* Add a comment to an existing discussion thread on a block.
For more information, check out the new [guide](/guides/data-apis/working-with-comments) or dive straight into the [API reference](/reference/create-a-comment).
### Features
* Released a new version of the API, `2022-06-28`. Previous versions of the API are still supported. Read more about the new version [here](/changelog/releasing-notion-version-2022-06-28).
* Created a new template repository for getting started with the Notion API and the official SDK. [Find it here.](https://github.com/makenotion/notion-sdk-typescript-starter)
### Bug fixes and performance improvements
* Exported many more named types for API response objects in the [official SDK](https://github.com/makenotion/notion-sdk-js).
* Fixed a bug in the official SDK where some API requests would not work due a capitalization issue. (This was a community-submitted PR; thank you @dvanoni!)
### Releasing Notion-Version 2022-06-28
**Update from August 31, 2022**: Page properties can now be retrieved using the page, query database, and search endpoints, in addition to the page properties endpoint.
Today we’re releasing Notion-Version `2022-06-28` with the following backwards incompatible changes:
* Page properties must be retrieved using the page properties endpoint.
* Parents are now always direct parents; a parent field has been added to block.
* Database relations have a type of `single_property` and `dual_property`.
Read more about each of these changes below.
### Page properties must be retrieved using the page properties endpoint
Previously, the [page object](/reference/page) returned from page endpoints, as well as the query database and search endpoint, returned a `properties` field that contained all the page’s properties along with its value:
```json Previous Version Response theme={null}
"properties": {
"Name": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Avocado",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Avocado",
"href": null
}
]
}
}
```
While convenient, returning accurate results for all properties resulted in bad performance and timeouts for larger databases or pages with lots of mentions. To combat performance, on March 1st, we [added a disclaimer](/reference/property-value-object) that page objects stopped returning accurate results for pages with more than 25 mentions to other objects (which affected properties of type `title`, `rich_text`, `relation`, `people`, `rollup`, and `formula`).
In October 2021, [we introduced](/changelog/retrieve-page-property-values) a way to more accurately retrieve individual page properties via the [retrieve a page property item endpoint](/reference/retrieve-a-page-property). With this endpoint, we’re able to paginate complex properties that involve additional look-ups.
With version `2022-06-28`, the `type` and `property` value from page objects are removed. Thus moving forward, all property value retrieval must happen through the retrieve a page property item endpoint.
```json New Version Response theme={null}
"properties": {
"Name": {
"id": "title"
}
}
```
For more examples of how to use the retrieve a page property item endpoint, our [SDK examples](/page/examples) have been updated to use the retrieve a page property item endpoint.
For more details about why page properties are so complex, we wrote about it in our [“Creating the Notion API”](https://www.notion.so/blog/creating-the-notion-api) blog post.
### Parents are now always direct parents; a parent field has been added to block
Previously, when accessing the parent of a database or page, that parent was always either a page, database, or workspace. This is un-faithful to the actual data model of Notion, where the parent may also be another block; for example, you can nest a page under a toggle block.
The parent field for page and database has been changed so that it is now always the direct parent of that page or database, and a new parent type has been added: `block_id`.
Additionally, a `parent` field has been added to the block object. Together, these changes allow you to fully traverse Notion’s tree.
To emulate the previous behavior of retrieving the page, database, or space parent, you may traverse up the tree using the retrieve a block endpoint. If the parent ≠ one of those types, retrieve the parent block until it is.
Read more about parent types [here](/reference/parent-object).
### Database relations have a type of `single_property` and `dual_property`
Relation properties in databases objects now have a type of `single_property` or `dual_property`. These can be used to create one way relations between databases as well as two way relations within a database.
### New version of the JavaScript SDK
Coinciding with all of these changes, we've released a new major version (v2.0.0) of the [Notion JavaScript SDK](https://github.com/makenotion/notion-sdk-js). To upgrade to this new version, run `npm install @notionhq/client@latest` or `yarn upgrade --latest @notionhq/client` from within your repository.
### Features
* Added limited readonly support for database status properties. Read more about status properties [here](https://www.notion.so/help/guides/status-property-gives-clarity-on-tasks).
* Status property values are returned in the [Retrieve a page](/reference/retrieve-a-page) endpoint. See [Property values](/reference/property-value-object) for more information.
* Status property configuration is not supported yet. See [Property object](/reference/property-object) for more information.
### Bug Fixes
* Added a validation for adding new rollup properties that prevents creating a rollup of another rollup.
### Features
* Added support for creating inline databases with `is_inline`. Read more [here](/reference/database).
* Added support for reading and writing database descriptions with the `description` field. Read more [here](/reference/database).
### Bug fixes and performance improvements
* The public API once again supports inline`mailto` links in rich text.
### Bug fixes and performance improvements
* We now validate URLs used to create inline text links in the public API. For more details on inline links in rich text, see the [Rich text object](/reference/rich-text) documentation.
* The [Search](/reference/post-search) endpoint now returns fuzzier matches, including plurals and different verb tenses. This corresponds to fuzzier matches while searching in the Notion app and should result in more search results overall for any given query.
* Fixed a bug where the integration page at integrations dashboard wouldn't load.
### Link Preview APIs
Today we’re excited to launch a new set of APIs for Link Preview integrations. Over the past six months, we launched link previews with tools like Slack, Trello, Figma, and Asana, allowing users to preview authenticated content in a new structured block.
We built link previews to make it easy for users to easily share information in one place using a link. But with a regular link, the information would become automatically stale, making it difficult to share the latest updates among teams. Link previews let supported services share authenticated, up-to-date content inside Notion.
Learn more about link previews [here](/page/link-previews-api).
### Bug fixes and performance improvements
* We've shipped a couple of improvements under the hood to make the search and query database endpoints faster. We're actively looking into 500s and timeouts on the query database endpoint in particular.
* Fixed a bug in the OAuth page picker where Shared pages wouldn't load until the workspace switcher was clicked
### Bug fixes and performance improvements
* Fixed a bug where some rollups and relations appeared empty when they shouldn't have.
* Fixed a bug in the query database endpoint where an invalid pagination cursor was being returned.
There was a [company-wide product bug bash](https://www.notion.so/releases)! As a result nothing API-specific to share for these 2 weeks, but we've been hard at work improving test coverage and paring down tech debt.
### Features
* You can now filter databases on the created at and last edited at timestamps, even if they don't have a corresponding property of that type. Read more [here](/reference/post-database-query-filter#timestamp).
* A [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) response header is now being sent with rate limited request responses. The value of this field is set as an integer number of seconds (in decimal). Requests made after waiting this minimum amount of time should not be rate limited. Read more about our rate limits [here](/reference/request-limits#rate-limits).
### Bug fixes and performance improvements
* Stopped throwing an error when rendering property formulas that hadn't been set up yet in the [Retrieve a page property item](/reference/retrieve-a-page-property) endpoint. These formulas now return `null` values.
### Query a database endpoint supports filtering by timestamp
When [querying a database](/reference/post-database-query) using filters, you previously were only able to build filters using properties that were explicitly defined in the database schema. We've added a new type of filter for the created timestamp and last edited timestamp of any page within the database. This means you can filter by these attributes, even if the database doesn't have a "Created time" or "Last edited time" *property*.
You can read more about this filter type [here](/reference/post-database-query-filter#timestamp), but as a preview here is how you would filter by the created timestamp:
```json JSON theme={null}
{
"filter": {
"timestamp": "created_time",
"created_time": {
"past_week": {}
}
}
}
```
And here's how you would filter by the last edited time:
```json JSON theme={null}
{
"filter": {
"timestamp": "last_edited_time",
"last_edited_time": {
"after": "2021-05-10"
}
}
}
```
Note that you can also use this filter type within a compound filter.
### Features
* Block colors are now supported in the API! Read more about it [here](/changelog/block-colors-are-now-supported-in-the-api).
### Bug fixes and performance improvements
* Rich text objects now properly include template mentions. Read more about this type of text object [here](/reference/rich-text#template-mention-type-object).
### Block colors are now supported in the API
We have added support for block colors in the Notion Public API. There is now a `color` keyword for the following block types: `paragraph`, `heading_1`, `heading_2`, `heading_3`, `bulleted_list_item`, `numbered_list_item`, `to_do`, `toggle`, `callout`, `quote`, and `table_of_contents`. For these block types, the block color is returned in the [block object](/reference/block), and you can use the [update block](/reference/update-a-block), [append block children](/reference/patch-block-children), and [create page](/reference/post-page) endpoints to update the color of existing blocks and create new blocks with color.
The colors supported are `default`, `gray`, `brown`, `orange`, `yellow`, `green`, `blue`, `purple`, `pink`, `red`, `gray_background`, `brown_background`, `orange_background`, `yellow_background`, `green_background`, `blue_background`, `purple_background`, `pink_background`, and `red_background`.
```json Example Block expandable theme={null}
{
"object": "block",
"id": "79bc0ae2-b002-4ecd-92db-870354734aaf",
"created_time": "2022-03-03T22:49:00.000Z",
"last_edited_time": "2022-03-03T22:49:00.000Z",
"created_by": {
"object": "user",
"id": "914ff1b3-45c7-48dc-b2c2-be37d21e7695"
},
"last_edited_by": {
"object": "user",
"id": "914ff1b3-45c7-48dc-b2c2-be37d21e7695"
},
"has_children": false,
"archived": false,
"type": "callout",
"callout": {
"rich_text": [
{
"type": "text",
"text": {
"content": "This block has color!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "This block has color!",
"href": null
}
],
"icon": {
"type": "emoji",
"emoji": "💡"
},
"color": "green_background"
}
}
```
### Updated Developers Terms
With the API officially out of beta, Notion has updated our developer terms of service as of March 1st, 2022. View our updated terms [here](https://www.notion.so/notion/Developer-Terms-ba4131408d0844e08330da2cbb225c20).
**The API is officially out of beta!**
Read more about it [here](https://www.notion.so/blog/api-ga).
### Features
* We now have a [roadmap](/page/changelog), so you have a better sense of what we'll be building next.
* We released a new version of the API, `2022-02-22`. This version makes our requests and responses more consistent across properties, blocks, and filters, and officially deprecates the list databases endpoint. Read more [here](/changelog/releasing-notion-version-2022-02-22).
* We now show the public API status independently of Notion's status on [https://status.notion.so/](https://status.notion.so/).
* Added `created_by` and `edited_by` to pages, blocks, and databases, and added `archived` to databases. Read more [here](/changelog/created-by-and-last-edited-by-properties-in-block-page-and-database-objects).
* Added new ways for admins of Enterprise workspaces to view and control the integrations installed in their workspaces. Read more [here](https://www.notion.so/help/add-and-manage-integrations-with-the-api).
* Added more information to paginated responses to make it easier to fetch complete responses for complex property types. Read more [here](/reference/pagination).
### Bug fixes and performance improvements
* Fixed a bug where pages and databases with archived (i.e. trashed) ancestors would show `archived: false`. They now show `archived: true` because they are, in fact, archived.
* Improved API performance when rendering users who are members in the space. This affects all user, block, and page-related endpoints since users can be mentioned in both page properties and rich text.
* Added a message about sharing relevant pages and databases with a bot in the 404 not found error message. We found that this was one of the more common reasons for API users to get a 404 when calling the API.
* Fixed a bug where bots could be given a more restrictive "Can edit content" access on child databases, which prevented some bots with write access from being able to update the database schema.
* Fixed a bug where user mentions failed with "user not found" when creating new blocks, even if those users should have been visible to the bot.
* Fixed a bug where malformed properties in a single page would cause an entire request to the query database endpoint to fail.
* Fixed a bug where it was possible to update a page/database in the trash. Attempting to update a trashed page or database now returns a validation error.
* Fixed a bug in the get page property endpoint where retrieving a rollup property which referenced a relation containing pages the bot did not have access to skipped those pages and returned an incorrect result. We now return a validation error.
* Fixed a bug in the get page property endpoint where retrieving a formula property whose depth exceeds what we can compute in the API simply returned the wrong value. We now return a validation error.
### Created by and last edited by properties in Block, Page and Database objects
We have added `created_by` and `last_edited_by` properties for [block](/reference/block), [page](/reference/page) and [database objects](/reference/database) corresponding to the users who have created or last edited these objects. Both properties are [user objects](/reference/user) which will contain `object` and `id` keys. This is a backwards compatible change that is available in older versions of the API as well.
```json Example page object expandable theme={null}
{
"object": "page",
"id": "e722caec-ae02-4a41-9bbd-286f65f8dca4",
"created_time": "2022-02-15T21:24:00.000Z",
"last_edited_time": "2022-02-17T22:40:00.000Z",
"created_by": {
"object": "user",
"id": "71e95936-2737-4e11-b03d-f174f6f13087"
},
"last_edited_by": {
"object": "user",
"id": "5ba97cc9-e5e0-4363-b33a-1d80a635577f"
},
"cover": null,
"icon": null,
"parent": {
"type": "page_id",
"page_id": "a7e32210-c151-4b96-8b94-ea659b1e8e4f"
},
"archived": false,
"properties": {
"title": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Tasks",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Tasks",
"href": null
}
]
}
},
"url": "https://notion.so/Tasks-e722caecae024a419bbd286f65f8dca4"
}
```
```json Example block object expandable theme={null}
{
"object": "block",
"id": "9bc30ad4-9373-46a5-84ab-0a7845ee52e6",
"created_time": "2021-03-16T16:31:00.000Z",
"created_by": {
"object": "user",
"id": "cb38e95d-00cf-4e7e-adce-974f4a44a547"
},
"last_edited_time": "2021-03-16T16:32:00.000Z",
"last_edited_by": {
"object": "user",
"id": "e79a0b74-3aba-4149-9f74-0bb5791a6ee6"
},
"has_children": false,
"type": "to_do",
"archived": false,
"to_do": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Lacinato kale",
"href": null
}
],
"checked": false
}
}
```
```json Example database object expandable theme={null}
{
"object": "database",
"id": "e8d49c2d-9644-4ba2-8511-918a62309665",
"cover": null,
"icon": null,
"created_time": "2022-02-15T21:09:00.000Z",
"created_by": {
"object": "user",
"id": "71e95936-2737-4e11-b03d-f174f6f13087"
},
"last_edited_by": {
"object": "user",
"id": "5ba97cc9-e5e0-4363-b33a-1d80a635577f"
},
"last_edited_time": "2022-02-17T18:43:00.000Z",
"title": [
{
"type": "text",
"text": {
"content": "Tasks",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Tasks",
"href": null
}
],
"properties": {
"Tags": {
"id": "keGJ",
"name": "Tags",
"type": "multi_select",
"multi_select": {
"options": []
}
},
"Name": {
"id": "title",
"name": "Name",
"type": "title",
"title": {}
}
},
"parent": {
"type": "page_id",
"page_id": "e722caec-ae02-4a41-9bbd-286f65f8dca4"
},
"url": "https://notion.so/e8d49c2d96444ba28511918a62309665",
"archived": false
}
```
We have also added a boolean `archived` property for [database objects](/reference/database) to denote if the database has been deleted. You can use the `archived` property to archive or unarchive a database and its descendants when [updating the database](/reference/update-a-database).
### Releasing Notion-Version 2022-02-22
**Notion's API versions**
As a reminder, we only version backwards incompatible changes, so generally, you still get access to new features we release on the API without needing to upgrade. You can use different version headers for each request, so you can upgrade incrementally to get to the latest version.
We're releasing Notion-Version `2022-02-22` with the following *backwards incompatible* changes:
* `text` in blocks has been renamed to `rich_text`, to be consistent with the database property type.
* Query database filter changes:
* `phone` and `text` are no longer supported in query database filters when filtering by `phone_number` and `rich_text` properties. Use `phone_number` and `rich_text` instead.
* `rollup` query database filters no longer accept the `text` keyword. Use `rich_text` instead.
* `formula` query database filters no longer accept the `text` keyword. Use `string` instead.
* `property_item` objects now return a `type`, `next_url`, and `id`.
* Deprecated the List Databases API endpoint.
#### The `text` property in content blocks has been renamed to `rich_text`
To be consistent with the database property type, we have renamed the `text` property to `rich_text`. This affects the following block types: `paragraph`, `heading_1`, `heading_2`, `heading_3`, `callout`, `quote`, `bulleted_list_item`, `numbered_list_item`, `to_do` ,`toggle`, `code` ,`template`.
Here is an example of the previous `text` property:
```json Previous paragraph block theme={null}
{
"type": "paragraph",
//...other keys excluded
"paragraph": {
"text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
}]
}
}
```
Here is an example of the updated `rich_text` property:
```json Updated paragraph block theme={null}
{
"type": "paragraph",
//...other keys excluded
"paragraph": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
}]
}
}
```
#### Query database filter changes
*`phone` and `text` no longer supported*
Version 2022-02-22 no longer supports `phone` and `text` property filters in the query database endpoint. For consistency with the database property types, use `phone_number` and `rich_text` instead when filtering on `phone_number` and `rich_text` properties.
More concretely, this query database filter will throw a validation error:
```json JSON theme={null}
{
"filter": {
"and": [
{
"property": "Phone number",
"phone": {
"equals": "1112223333"
}
}
]
}
}
```
This query database filter will succeed:
```json JSON theme={null}
{
"filter": {
"and": [
{
"property": "Phone number",
"phone_number": {
"equals": "1112223333"
}
}
]
}
}
```
*`rollup` property filters accept `rich_text` instead of `text`*
Rollup property filters must now be constructed with the `rich_text` keyword instead of the `text` keyword if the value of the rollup is an array of `rich_text`. Put concretely, if a page's rollup property is rendered like so:
```json JSON expandable theme={null}
"rollup property": {
"id": "~%5Bw%5C",
"type": "rollup",
"rollup": {
"type": "array",
"array": [
{
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": {
"content": "update text 2",
"link": null
},
"annotations": {
"bold": true,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "red"
},
"plain_text": "update text 2",
"href": null
}
]
},
{
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": {
"content": "another text",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "another text",
"href": null
}
]
}
],
"function": "show_original"
}
}
```
This filter will no longer work in version 2022-02-22:
```json JSON theme={null}
{
"filter": {
"property": "rollup property",
"rollup": {
"any": {
"text": {
"contains": "update text"
}
}
}
}
}
```
Instead, write:
```json JSON theme={null}
{
"filter": {
"property": "rollup property",
"rollup": {
"any": {
"rich_text": {
"contains": "update text"
}
}
}
}
}
```
*`formula` property filters accept `string` instead of `text`*
Rollup property filters must now be constructed with the `string` keyword instead of the `text` keyword if the value of the formula is a `string`. Put concretely, if a page's formula property is rendered like so:
```json JSON theme={null}
"formula property": {
"id": "m%5D%3F%5C",
"type": "formula",
"formula": {
"type": "string",
"string": "update text 2,another text"
}
}
```
This filter will no longer work in version 2022-02-22:
```json JSON theme={null}
{
"filter": {
"property": "formula property",
"formula": {
"text": {
"contains": "update text"
}
}
}
}
```
Instead, write this:
```json JSON theme={null}
{
"filter": {
"property": "formula property",
"formula": {
"string": {
"contains": "update text"
}
}
}
}
```
### Property list items now have types
Property item lists now always have type `property_item`. Rollup aggregations are now returned inside that type.
We've also added the property `id` field and the `next_url` to fetch the next set of property items.
Here is an example of a previous `rollup` `property_item` list:
```json JSON expandable theme={null}
{
"object": "list",
"results": [
{
"object": "property_item",
"type": "relation",
"relation": {
"id": "83f92c9d-523d-466e-8c1f-9bc2c25a99fe"
}
},
...
],
"next_cursor": "some-next-cursor-value",
"has_more": true,
"rollup": {
"type": "date",
"date": {
"start": "2021-10-07T14:42:00.000+00:00",
"end": null
},
"function": "latest_date"
},
"type": "rollup"
}
```
Here is an example of the updated `rollup` `property_item` list:
```json JSON expandable theme={null}
{
"object": "list",
"results": [
{
"object": "property_item",
"id": "xYz890",
"type": "relation",
"relation": {
"id": "83f92c9d-523d-466e-8c1f-9bc2c25a99fe"
}
},
...
],
"next_cursor": "some-next-cursor-value",
"has_more": true,
"type": "property_item",
"property_item": {
"id": "aBcD123"
"next_url": "https://api.notion.com/v1/pages/b55c9c91-384d-452b-81db-d1ef79372b75/properties/aBcD123?start_cursor=some-next-cursor-value",
"type": "rollup",
"rollup": {
"type": "date",
"date": {
"start": "2021-10-07T14:42:00.000+00:00",
"end": null
},
"function": "latest_date"
}
},
}
```
### Deprecated the List Databases endpoint
List all [Databases](/reference/database) endpoint is removed starting in this version. You can use the [Search API](/reference/post-search) for this functionality instead. The List Databases endpoint only returns explicitly shared databases, while search will also return child pages and databases within explicitly shared pages.
**We're trying something new**
We're experimenting with publishing biweekly changelogs in addition to our existing changelogs about new features. The biweekly changelogs will include bug fixes and improvements that are not big enough to justify their own changelog entry.
The timing may be somewhat irregular until we smooth the process out, but we hope to align on a regular schedule soon. This is our first regular changelog entry; we hope you find it useful.
### Bug fixes and performance improvements
* We added an optimization for search when filtering by pages or databases. This should particularly help latency when using search to power a database picker in a large workspace. For more details about search and how to optimize search requests, see the [search documentation](/reference/post-search).
* We fixed an issue where fetching an embed block containing an uploaded file returned the wrong file URL.
### Caption property is now supported for code block type
We have added support for adding, updating, and retrieving the `caption` property for `code` block types.
Below is an example response from [append block children](/reference/patch-block-children) containing a code block, with a caption, uploaded to Notion.
```json JSON expandable theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "block-id",
"created_time": "2021-10-14T18:10:00.000Z",
"last_edited_time": "2021-10-14T18:10:00.000Z",
"has_children": false,
"archived": false,
"type": "code",
"code": {
"caption": [
{
"type": "text",
"text": {
"content": "Hello Caption!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Hello Caption!",
"href": null
}
],
"text": [
{
"type": "text",
"text": {
"content": "const foo = \"bar\"",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "const foo = \"bar\"",
"href": null
}
],
"language": "javascript"
}
},
],
"next_cursor": null,
"has_more": false
}
```
We have added support for simple tables in the API.
### Simple tables and simple table rows
Tables are parent blocks for table row children. They can only contain children of type `table_row`.
When creating a table block via the [Append block children](/reference/patch-block-children) endpoint, the `table` must have at least 1 `table_row` whose `cells` array has the same length as the `table_width`.
To fetch content for a `table`, fetch the the `table_row` children via [Retrieve block children](/reference/get-block-children). The `table` block itself only contains formatting data, no content.
Table block example:
```json JSON theme={null}
{
"type": "table",
"table": {
"table_width": 3,
"has_column_header": false,
"has_row_header": false
}
}
```
Table row block example:
```json JSON expandable theme={null}
{
"type": "table_row",
"table_row": {
"cells": [
[
{
"type": "text",
"text": {
"content": "column 1 content",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "column 1 content",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "column 2 content",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "column 2 content",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "column 3 content",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "column 3 content",
"href": null
}
]
]
}
}
```
For more details, refer to the [Block object](/reference/block) docs.
Both public and internal integrations now support having more granular capabilities, which enforce what an integration can do and see in a Notion workspace. These capabilities when put together enforce which API endpoints an integration can call, and what content and user related information they are able to see. For further information on capabilities and best practices, see the [capabilities reference](/reference/capabilities).
### Content capabilities
Integrations can have any combination of read content, insert content, or update content capabilities.
* The **read content** capability gives the integration access to read existing content in a Notion workspace.
* The **insert content** capability gives the integration permission to create new content in a Notion workspace.
* The **update content** capability gives the integration permission to update existing content in a Notion workspace.
### User capabilities
Integrations have different levels of user capabilities, which affect how [user objects](/reference/user) are returned from the Notion API:
* No user information - the integration will not be able to request any information about users. User objects will not include information about the user, including name, profile images, or their email address.
* User information without email addresses - user objects will include other information about the user, including their name or profile images, but omit the email address.
* User information with email addresses - user objects will include all information about the user, including name, profile images, and their email address.
### Limitations
An installed integration can never capabilities will never supersede the capabilities of the user who owns the integration. For example, an integration cannot insert or update on a page if the owner has read-only access.
### Existing integrations
All existing integrations will continue to have the same functionality as before. Any integrations created before December 15, 2021 automatically will have all content capabilities, and user capabilities that give access to user information including email addresses.
### Updating integrations
Update the capabilities on an existing integration through integrations dashboard. After updating a public integration's capabilities, users will need to re-authenticate with the integration to apply the new capabilities to their installation. After re-authenticating a public integration with changed capabilities, or updating an internal integration with changed capabilities, the new capabilities will apply to all pages already shared with the integration. For more information on setting capabilities see the [Authorization](/guides/get-started/authorization) guide.
### Time zone support
We have added an optional `time_zone` field (based on the [IANA database](https://www.iana.org/time-zones) time zone values) to the Date objects. Developers can now explicitly set the time zones of Date property values using the `time_zone` field. Once this property is set explicitly, users will be able to see the same time zone in the app. When time zone information is provided in this method, `start` and `end` cannot contain [UTC offset](https://en.wikipedia.org/wiki/UTC_offset)s. In addition when time zone information is provided in dates, `start` and `end` cannot be dates without time information (i.e. `"2020-12-08"`).
The public API will always return the `time_zone` field as `null` when rendering dates and time zone will be displayed as a [UTC offset](https://en.wikipedia.org/wiki/UTC_offset) in the `start` and `end` date fields.
### Synced Block, Link to Page and Template block types are now supported in the API
We have added support for adding and retrieving `synced_block`, `link_to_page` and `template` block types.
#### `synced_block` block type
Similar to the UI, there are two versions of a `synced_block` -- the original block that was created first and doesn't yet sync with anything else, and the reference blocks that are synced to the original synced block.
**Original Synced Block** To create a `synced_block`, the developer needs to create an original synced block. Developers will be able to identify the original `synced_block` because it does not "sync\_from" any other block (i.e. the `synced_from` property is set to `null`).
This is an example of an "original" `synced_block`. Note that all of the blocks available to be synced in another `synced_block` are captured in the `children` property.
```json synced_block (original) theme={null}
{
"type": "synced_block",
"synced_block": {
"synced_from": null,
"children": [
{
"callout": {
"text": [
{
"type": "text",
"text": {
"content": "Callout in synced block"
}
}
]
}
}
]
}
}
```
**Reference Synced Blocks** To sync the content of the original `synced_block` with another `synced_block`, the developer simply needs to refer to that `synced_block` using the `synced_from` property.
Below is an example of a `synced_block` referring to another `synced_block`. Note that only "original" synced blocks can be referenced in the `synced_from` property.
```json synced_block (reference to original) in request body theme={null}
{
"type": "synced_block",
"synced_block": {
"synced_from": {
"block_id": "original_synced_block_id"
}
}
}
```
Below is the example response after creating the `synced_block` above. We can tell that the content from the original synced block is synced with this one because this block has children even though we didn't explicitly set the children in the body of our API call above (i.e. `has_children` property on the reference block is `true`).
```json synced_block (reference to original) in response body theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "block_id",
"created_time": "2021-11-17T22:17:00.000Z",
"last_edited_time": "2021-11-17T22:17:00.000Z",
"has_children": true,
"archived": false,
"type": "synced_block",
"synced_block": {
"synced_from": {
"type": "block_id",
"block_id": "original_synced_block_id"
}
}
}
],
"next_cursor": null,
"has_more": false
}
```
**Important notes**
1. The bot must have access to both the original and reference synced blocks
2. Similar to the UI, we don't support changes to `synced_from` at this time
#### `link_to_page` block type
We have added support for adding and retrieving `link_to_page` block types. Using this block type, developers can now create page links to other pages (using the `page_id` property) and full page databases (using the `database_id` property).
Below is an example request body for the [append block children](/reference/patch-block-children) endpoint containing a `link_to_page` block type.
```json link_to_page block type used in request body theme={null}
{
"children": [
{
"type": "link_to_page",
"link_to_page": {
"type": "page_id",
"page_id": "61cca5bd-c8c6-4fcc-b517-514da3b8b1e0"
}
}
]
}
```
#### `template` block type
We have added support for adding and retrieving `template` block types. Using this block type, developers can now create template that duplicates the its children blocks.
Below is an example request body for the [append block children](/reference/patch-block-children) endpoint containing a `template` block type.
```json template_block type used in request body expandable theme={null}
{
"children": [
{
"type": "template",
"template": {
"text": [
{
"type": "text",
"text": {
"content": "Create callout template"
}
}
],
"children": [
{
"callout": {
"text": [
{
"type": "text",
"text": {
"content": "Placeholder callout text"
}
}
]
}
}
]
}
}
]
}
```
The public API now supports returning `link_preview` blocks and mentions found in `rich_text`! Previously these blocks had type `unsupported` and mentions were skipped in `rich_text`. Note: `link_preview`s cannot be created via the API, only returned in responses.
See the documentation in [blocks](/reference/block#link-preview) and [`rich_text`](/reference/rich-text#link-preview-mention-type-object) for more information.
```json JSON theme={null}
{
"type": "link_preview",
//...other keys excluded
"link_preview": {
"url": "https://github.com/example/example-repo/pull/1234"
}
}
```
We have added support for `column_list` and `column` block types.
You can now add Column Lists and Columns to pages and other block types.
Column Lists are parent blocks for column children. They can only contain children of type `column`.
Columns are parent blocks for any supported block children, excluding `column`s. They can only be appended to `column_list`s.
When initially creating a column list block via [Append block children](/reference/patch-block-children), the column list must have at least 2 columns, and those columns must have at least one child each.
When fetching content for a column\_list, first fetch the the column children via [Retrieve block children](/reference/get-block-children). Then fetch the children for each column block.
Below is an example request body for appending `column_list` and nested `column` children.
```json JSON expandable theme={null}
{
"children": [
{
"object": "block",
"type": "column_list",
"column_list": {
"children": [
{
"object": "block",
"type": "column",
"column": {
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"text": [
{
"type": "text",
"text": {
"content": "some text here"
}
}
]
}
}
]
}
},
{
"object": "block",
"type": "column",
"column": {
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"text": [
{
"type": "text",
"text": {
"content": "some text here"
}
}
]
}
}
]
}
}
]
}
}
]
}
```
Below is an example response of appending `column_list` children.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "ca042aa7-2e23-4541-8059-abff360d6752",
"created_time": "2021-10-25T17:00:00.000Z",
"last_edited_time": "2021-10-25T17:00:00.000Z",
"has_children": true,
"archived": false,
"type": "column_list",
"column_list": {}
}
],
"next_cursor": null,
"has_more": false
}
```
Below is an example request body for appending `column` children. Note that the parent that is being added to must be a block of type `column_list`.
```json JSON expandable theme={null}
{
"children": [
{
"object": "block",
"type": "column",
"column": {
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"text": [
{
"type": "text",
"text": {
"content": "some text here"
}
}
]
}
}
]
}
}
]
}
```
Below is an example response of appending `column` children.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "f40c3a13-30d1-4594-bd6f-cdbc15b2c006",
"created_time": "2021-10-25T21:25:00.000Z",
"last_edited_time": "2021-10-25T21:25:00.000Z",
"has_children": true,
"archived": false,
"type": "column",
"column": {}
}
],
"next_cursor": null,
"has_more": false
}
```
### Validation on embed block URLs
The public API will now return errors on embeds blocks that are not supported by the public API. The supported embed block types (as listed and kept up to date in the [Block Object](/reference/block#embed) documentation):
* Framer
* Twitter (tweets)
* Google Drive documents
* Gist
* Figma
* Invision,
* Loom
* Typeform
* Codepen
* PDFs
* Google Maps
* Whimisical
* Miro
* Abstract
* Excalidraw
* Sketch
* Replit
Previously failed embeds would return a successful request, but produce an error in the Notion Application. Failed embed requests will now return a 400 Client Error.
For non embed URLs, consider using the `bookmark` or `image` block types.
### Dates with times and timezones are now supported on Database Date Filters
Previously, the date filters `equals`, `after`, `before`, `on_or_after`, and `on_or_before` only supported dates without times nor timezones.
```json JSON theme={null}
{
"filter": {
"or": [
{
"property": "My Time Property",
"date": {
"before": "2021-10-20"
}
}
]
},
"sorts": []
}
```
Now the database date filters can accept ISO 8601 dates with timestamps and timezones.
```json JSON theme={null}
{
"filter": {
"or": [
{
"property": "My Time Property",
"date": {
"before": "2021-10-15T12:00:00-07:00"
}
}
]
},
"sorts": []
}
```
**How Dates with times are compared**
Date time comparisons are done with millisecond precision. If no timezone is provided, the default is UTC.
**Equals Date filter**
If a date without a time is provided to the `equals`, the comparison is done against the start and end of the UTC date provided (inclusive). If a date with a time is provided, the comparison is done with millisecond precision. If no timezone is provided, the default timezone is UTC.
### Breadcrumb block types are now supported in the API
We have added support for adding and retrieving `Breadcrumb` block types.
You can now can add Breadcrumb blocks to pages and other blocks.
Below is an example response from [Append block children](/reference/patch-block-children) containing a Breadcrumb block uploaded to Notion.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "block-id",
"created_time": "2021-10-14T18:10:00.000Z",
"last_edited_time": "2021-10-14T18:10:00.000Z",
"has_children": false,
"archived": false,
"type": "breadcrumb",
"breadcrumb": {}
}
],
"next_cursor": null,
"has_more": false
}
```
### Table of contents and divider block types are now supported
We have added support for adding and retrieving `Table of Contents` and `Divider` block types.
### Table of Contents blocks
You can now can add Table of Contents blocks to pages and other blocks.
Below is an example response from [Append block children](/reference/patch-block-children) containing a Table of Contents block uploaded to Notion.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "block-id",
"created_time": "2021-10-14T18:10:00.000Z",
"last_edited_time": "2021-10-14T18:10:00.000Z",
"has_children": false,
"archived": false,
"type": "table_of_contents",
"table_of_contents": {}
}
],
"next_cursor": null,
"has_more": false
}
```
### Divider blocks
You can now can add Divider blocks to pages and other blocks.
Below is an example response from [Append block children](/reference/patch-block-children) containing a Divider block uploaded to Notion.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "block-id",
"created_time": "2021-10-14T18:10:00.000Z",
"last_edited_time": "2021-10-14T18:10:00.000Z",
"has_children": false,
"archived": false,
"type": "divider",
"divider": {}
}
],
"next_cursor": null,
"has_more": false
}
```
### Users can now add Equation Blocks, Embed, Bookmark, and Media Blocks
We have added support for retrieving, adding and updating Equation Blocks. We have also added support for updating Embed, Bookmark and Media (including image, video, audio, file, pdf) block types.
#### Equation Blocks
You can now can add, retrieve, and update equation blocks when using the [Append block children](/reference/patch-block-children) , [Retrieve block children](/reference/get-block-children) and [Update block](/reference/update-a-block) API endpoints.
Below is an example response from [Append block children](/reference/patch-block-children) containing an equation block uploaded to Notion.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "2d97bf53-7e79-4efd-a6b2-53c0026ed1b5",
"created_time": "2021-10-11T17:56:00.000Z",
"last_edited_time": "2021-10-11T17:56:00.000Z",
"has_children": false,
"archived": false,
"type": "equation",
"equation": {
"expression": "e = mc^2"
}
}
],
"next_cursor": null,
"has_more": false
}
```
#### Media Blocks (video, audio, image, file, pdf)
You can now can update media blocks when using [Update block](/reference/update-a-block).
**Only media blocks of type external are supported**
Updated Media blocks must be of type "external" and must reference an external URL. File upload is not currently supported.
Below is an example response from [Update block](/reference/update-a-block) containing a video block uploaded to Notion.
```json JSON expandable theme={null}
{
"object": "block",
"id": "37af9d6e-290a-488b-9d53-fb4b983f8289",
"created_time": "2021-10-12T23:11:00.000Z",
"last_edited_time": "2021-10-12T23:12:00.000Z",
"has_children": false,
"archived": false,
"type": "video",
"video": {
"caption": [
{
"type": "text",
"text": {
"content": "My caption",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "My caption",
"href": null
}
],
"type": "external",
"external": {
"url": "https://www.youtube.com/watch?v=aA7si7AmPkY"
}
}
}
```
#### Embed and Bookmark Block Types
You can now can update embed and bookmark blocks when using [Update block](/reference/update-a-block).
Below is an example response from [Update block](/reference/update-a-block) containing a bookmark block uploaded to Notion.
```json JSON expandable theme={null}
{
"object": "block",
"id": "050010f8-d164-4f5d-85a0-fc9db4598107",
"created_time": "2021-10-12T23:05:00.000Z",
"last_edited_time": "2021-10-12T23:06:00.000Z",
"has_children": false,
"archived": false,
"type": "bookmark",
"bookmark": {
"caption": [
{
"type": "text",
"text": {
"content": "My caption",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "My caption",
"href": null
}
],
"url": "http://www.notion.so"
}
}
```
### Users can now add and update Callout and Quote block types
**New API endpoints and block types not supported in older versions of the API as of September 28**
As of September 28, 2021, new block types and API endpoints will *not* be supported in older versions of the API. If you're currently on version `2021-05-11` or `2021-05-13`, upgrade to `2021-08-16` to take advantage of the new block types in this changelog and any other block types or endpoints introduced after September 28.
API functionality introduced before September 28 will continue to work with older API versions.
We have added support for retrieving, adding and updating quote and callout block types.
#### Quote blocks
You can now can add and retrieve quote blocks when using [Append block children](/reference/patch-block-children) and [Retrieve block children](/reference/get-block-children).
Below is an example response from [Append block children](/reference/patch-block-children) containing a quote block uploaded to Notion.
```json JSON expandable theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "block-id",
"created_time": "2021-10-07T19:21:00.000Z",
"last_edited_time": "2021-10-07T19:21:00.000Z",
"has_children": false,
"archived": false,
"type": "quote",
"quote": {
"text": [
{
"type": "text",
"text": {
"content": "The digital revolution is far more significant than the invention of writing or even of printing.",
"link": null
},
"annotations": {
"bold": false,
"italic": true,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "The digital revolution is far more significant than the invention of writing or even of printing.",
"href": null
},
{
"type": "text",
"text": {
"content": "\n-Douglas Engelbart",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "\n-Douglas Engelbart",
"href": null
}
]
}
}
],
"next_cursor": null,
"has_more": false
}
```
#### Callout blocks
You can now can add and retrieve callout blocks when using [Append block children](/reference/patch-block-children) and [Retrieve block children](/reference/get-block-children).
Below is an example response from [Retrieve block](/reference/retrieve-a-block) containing a callout block uploaded to Notion.
```json JSON expandable theme={null}
{
"object": "block",
"id": "block-id",
"created_time": "2021-10-07T19:23:00.000Z",
"last_edited_time": "2021-10-07T19:24:00.000Z",
"has_children": false,
"archived": false,
"type": "callout",
"callout": {
"text": [
{
"type": "text",
"text": {
"content": "Note: Something special is happening above",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Note: Something special is happening above",
"href": null
}
],
"icon": {
"type": "emoji",
"emoji": "💡"
}
}
}
```
### Retrieve page property item
Developers can now individually retrieve the value of their page properties with the [Retrieve a page property](/reference/retrieve-a-page-property) endpoint! This includes pagination through a list of property item objects for properties with long values or lots of page references such as formula, relations and rollups. See the [documentation](/reference/retrieve-a-page-property) for more info.
Use the [Retrieve a database](/reference/retrieve-a-database) endpoint to obtain the `property_id` .
**Simple Property Types**
Most properties will be identified by a `type` with the property value in the object found in key `{type}`.
*Example Request/Response*
```bash cURL theme={null}
curl --request GET \
--url http://localhost:3000/v1/pages/b55c9c91-384d-452b-81db-d1ef79372b75/properties/some-property-id \
--header 'Authorization: Bearer $NOTION_API_KEY' \
--header 'Notion-Version: 2021-08-16'
```
```json JSON theme={null}
{
"object": "property_item",
"type": "number",
"number": 2
}
```
**Paginated Property Types**
Properties of type `title`, `rich_text`, `relation` and `people` will return a paginated list of [Property Item Objects](/reference/retrieve-a-page-property#property-item-objects)
*Example List Response*
```json JSON expandable theme={null}
{
"object": "list",
"results": [
{
"object": "property_item",
"type": "rich_text",
"rich_text": {
"type": "text",
"text": {
"content": "Avocado ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Avocado ",
"href": null
}
},
... // additional results omitted.
],
"next_cursor": "some-next-cursor-value",
"has_more": true
}
```
**Rollup Property Types**
Rollups of type 'Show Original', 'Show unique', 'Count unique' and 'Median' return a flattened list of property items. All other rollups are return a list of relations and (after pagination) a [rollup property value]() of type `date` or `number`.
*Example Paginated Property Item Request/Response*
A rollup page property with an aggregation that requires additional pagination.
```bash cURL theme={null}
curl --request GET \
--url http://localhost:3000/v1/pages/b55c9c91-384d-452b-81db-d1ef79372b75/properties/some-property-id?page_size=10&start_cursor=some-cursor-value \
--header 'Authorization: Bearer $NOTION_API_KEY' \
--header 'Notion-Version: 2021-08-16'
```
```json JSON expandable theme={null}
{
"object": "list",
"results": [
{
"object": "property_item",
"type": "relation",
"relation": {
"id": "de5d73e8-3748-40fa-9102-f1290fe2444b"
}
},
{
"object": "property_item",
"type": "relation",
"relation": {
"id": "164325b0-4c9e-416b-ba9c-037b4c9acdfd"
}
},
{
"object": "property_item",
"type": "relation",
"relation": {
"id": "456baa98-3239-4c1f-b0ea-bdae945aaf33"
}
}
...
],
"next_cursor": "some-next-cursor-value",
"has_more": true,
"rollup": {
"type": "date",
"date": {
"start": "2021-10-07T14:42:00.000+00:00",
"end": null
},
"function": "latest_date"
},
"type": "rollup"
}
```
### Retrieve your token's bot user with GET /v1/users/me
If you're using Notion API version `2021-08-16`, you can now retrieve information about the bot associated with your API token, including its ID and the user who authorized it.
**Example request**
```bash cURL theme={null}
curl --request GET \
--url http://localhost:3000/v1/users/me \
--header 'Authorization: Bearer $NOTION_API_KEY' \
--header 'Notion-Version: 2021-08-16'
```
**Example response**
```json JSON expandable theme={null}
{
"object": "user",
"id": "16d84278-ab0e-484c-9bdd-b35da3bd8905",
"name": "pied piper",
"avatar_url": null,
"type": "bot",
"bot": {
"owner": {
"type": "user",
"user": {
"object": "user",
"id": "5389a034-eb5c-47b5-8a9e-f79c99ef166c",
"name": "christine makenotion",
"avatar_url": null,
"type": "person",
"person": {
"email": "christine@makenotion.com"
}
}
}
}
}
```
### New functionality not available to old API versions; code, inline databases, and database page block
**New API endpoints and block types not supported in older versions of the API as of September 28**
As of September 28, 2021, new block types and API endpoints will *not* be supported in older versions of the API. If you're currently on version `2021-05-11` or `2021-05-13`, upgrade to `2021-08-16` to take advantage of the new block types in this changelog and any other block types or endpoints introduced after September 28.
API functionality introduced before September 28 will continue to work with older API versions.
We have added support for retrieving, adding and updating code blocks, inline databases and database page blocks.
**Code blocks**
You can now can retrieve and add code blocks when using [Append block children](/reference/patch-block-children) and [Retrieve block children](/reference/get-block-children).
Below is an example response from [Retrieve block children](/reference/get-block-children) containing a code block uploaded to Notion.
```json JSON expandable theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "ee27cd42-eaad-467f-9956-c0aa4efa94b5",
"created_time": "2021-09-22T20:27:00.000Z",
"last_edited_time": "2021-09-27T19:25:00.000Z",
"has_children": false,
"archived": false,
"type": "code",
"code": {
"text": [
{
"type": "text",
"text": {
"content": "const a = 21\nconst b = a + 5",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "const a = 21\nconst b = a + 5",
"href": null
}
],
"language": "javascript"
}
},
],
"next_cursor": null,
"has_more": false
}
```
**Inline databases and database page blocks**
You can now can retrieve child database blocks when using [Retrieve block children](/reference/get-block-children) and [Retrieve block](/reference/retrieve-a-block).
**Updating `child_database` blocks**
To update `child_database` type blocks, use the [Update database](/reference/update-a-database) endpoint. Updating the block's `title` updates the text displayed in the associated `child_database` block.
Below is an example response from [Retrieve block children](/reference/get-block-children) containing a child database uploaded to Notion.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "0d6ff4f9-1211-4129-ab4a-19dfc33d4d7a",
"created_time": "2021-09-27T20:25:00.000Z",
"last_edited_time": "2021-09-27T20:25:00.000Z",
"has_children": false,
"archived": false,
"type": "child_database",
"child_database": {
"title": "My child database"
}
}
],
"next_cursor": null,
"has_more": false
}
```
### Workspace-level tokens for public integrations will be deprecated soon; migrate your OAuth flows
Starting today we will be changing who can authorize public integrations in Notion workspaces. The previously released authorization method will be fully deprecated on October 19.
#### About the change
Currently OAuth tokens function on a workspace level: only admins in a workspace can grant access and there can only be one token per workspace per integration. After a brief transition period (see "How to prepare for this change" below) we will be switching exclusively to user-level tokens. These can be granted by any admin or member in the workspace, and there can be as many tokens per workspace as there are admins and members in the workspace.
See the table for the differences between these two methods:
| | Workspace-level tokens (old) | User-level tokens (new) |
| :---------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Who can go through OAuth and grant access | Admins only | Admins and members |
| Number of access tokens per workspace | 1 | Up to N, where N is the number of admins and members |
| Who can go through OAuth and reauthorize access for a given token | Only the original user who went through OAuth to grant the token | Only the original user who went through OAuth to grant the token |
| OAuth token response | Contains an `owner` field with the value `{ workspace: true }` | Contains an `owner` field with the value `{ user: }` |
| What resources an integration has access to | Pages/databases the installing user chooses via the page picker during OAuth; pages/databases the installing user and other users in the workspace share with the integration via the Page menu; children of pages/databases that were shared with the integration | Pages/databases the installing user chooses via the page picker during OAuth; pages/databases the installing user shares with the integration via the Page menu; children of pages/databases that were shared with the integration |
| What an integration can do with resources it has access to | Read and write | Read and write |
#### How to prepare for this change:
This change only affects public integrations; that is, integrations that can be installed across many workspaces via OAuth. It does not affect internal integrations.
1. Ensure that you can store and handle multiple Notion API tokens per workspace where your integration is granted access. You may map tokens directly to the `bot_id` which is returned in the OAuth token response and is guaranteed to be unique per API token.
* To avoid overwriting tokens, do not map the token to the `workspace_id` returned in the OAuth token response, since a workspace may have multiple tokens. Do not map the token to the `owner.user.id` in the OAuth token response, since a user may install your integration in multiple workspaces.
2. Add `&owner=user` to your OAuth authorization URL (the url starting with `https://api.notion.com/v1/oauth/authorize`) once your application is ready for user-level tokens.
#### What to expect on October 19
On October 19, we will migrate all existing workspace-level tokens to user-level tokens. We will also default to creating user-level tokens when a user goes through OAuth, regardless of the `owner` parameter in the OAuth URL.
### Database objects now contain url
[Database objects](/reference/database) now return the web address of the database in the `url` key.
```json cURL theme={null}
{
"object": "database",
"id": "668d797c-76fa-4934-9b05-ad288df2d136",
"created_time": "2020-03-17T19:10:04.968Z",
"last_edited_time": "2020-03-17T21:49:37.913Z",
"parent": {
"type": "page_id",
"page_id": "48f8fee9-cd79-4180-bc2f-ec0398253067"
},
"icon": {
"type": "emoji",
"emoji": "🎉"
},
"cover": {
"type": "external",
"external": {
"url": "https://website.domain/images/image.png"
}
},
"url": "https://www.notion.so/668d797c76fa49349b05ad288df2d136",
"title": [
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Grocery List",
"href": null
}
],
"properties": {
"Name": {
"id": "title",
"type": "title",
"title": {}
},
"Description": {
"id": "J@cS",
"type": "rich_text",
"rich_text": {}
},
"In stock": {
"id": "{xY`",
"type": "checkbox",
"checkbox": {}
},
"Food group": {
"id": "TJmr",
"type": "select",
"select": {
"options": [
{
"id": "96eb622f-4b88-4283-919d-ece2fbed3841",
"name": "🥦Vegetable",
"color": "green"
},
{
"id": "bb443819-81dc-46fb-882d-ebee6e22c432",
"name": "🍎Fruit",
"color": "red"
},
{
"id": "7da9d1b9-8685-472e-9da3-3af57bdb221e",
"name": "💪Protein",
"color": "yellow"
}
]
}
},
"Price": {
"id": "cU^N",
"type": "number",
"number": {
"format": "dollar"
}
},
"Cost of next trip": {
"id": "p:sC",
"type": "formula",
"formula": {
"value": "if(prop(\"In stock\"), 0, prop(\"Price\"))"
}
},
"Last ordered": {
"id": "]\\R[",
"type": "date",
"date": {}
},
"Meals": {
"type": "relation",
"relation": {
"database": "668d797c-76fa-4934-9b05-ad288df2d136",
"synced_property_name": null
}
},
"Number of meals": {
"id": "Z\\Eh",
"type": "rollup",
"rollup": {
"rollup_property_name": "Name",
"relation_property_name": "Meals",
"rollup_property_id": "title",
"relation_property_id": "mxp^",
"function": "count"
}
},
"Store availability": {
"type": "multi_select",
"multi_select": {
"options": [
[
{
"id": "d209b920-212c-4040-9d4a-bdf349dd8b2a",
"name": "Duc Loi Market",
"color": "blue"
},
{
"id": "70104074-0f91-467b-9787-00d59e6e1e41",
"name": "Rainbow Grocery",
"color": "gray"
},
{
"id": "e6fd4f04-894d-4fa7-8d8b-e92d08ebb604",
"name": "Nijiya Market",
"color": "purple"
},
{
"id": "6c3867c5-d542-4f84-b6e9-a420c43094e7",
"name": "Gus's Community Market",
"color": "yellow"
}
]
]
}
},
"+1": {
"id": "aGut",
"type": "people",
"people": {}
},
"Photo": {
"id": "aTIT",
"type": "files",
"files": {}
}
}
}
```
### Users can now delete Block objects
The Notion API now supports the [Delete a block](/reference/delete-a-block) endpoint for all supported block types (include pages). The endpoint mirrors the behavior in the Notion application UI where items are added to the "Trash" bucket. In addition, the [Block object](/reference/block) now returns a boolean `archived` field to denote if the block has been deleted.
After deleting (archiving) the block, it can be unarchived using the [Update a block](/reference/update-a-block) or [Update page](/reference/patch-page) endpoint with the body `{ archived: false }`.
#### Example Request
```bash cURL theme={null}
curl 'https://api.notion.com/v1/blocks/9bc30ad4-9373-46a5-84ab-0a7845ee52e6' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H 'Notion-Version: 2021-08-16'
-X DELETE \
```
#### Example response
```json JSON theme={null}
{
"object": "block",
"id": "9bc30ad4-9373-46a5-84ab-0a7845ee52e6",
"created_time": "2021-03-16T16:31:00.000Z",
"last_edited_time": "2021-03-16T16:32:00.000Z",
"has_children": false,
"type": "to_do",
"archived": true
"to_do": {
"text": [
{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Lacinato kale",
"href": null
}
],
"checked": false
}
}
```
## September 9, 2021
### Relation and rollup properties can now be created in databases
When [creating](/reference/create-a-database) or [updating](/reference/update-a-database) databases, you can now add `relation` and `rollup` property types. Note that the related database must also be shared with the integration.
#### Example request
```bash cURL theme={null}
curl --location --request POST 'https://api.notion.com/v1/databases/' \
--header 'Authorization: Bearer '"$NOTION_API_KEY"'' \
--header 'Content-Type: application/json' \
--header 'Notion-Version: 2021-08-16' \
--data '{
"parent": {
"type": "page_id",
"page_id": "98ad959b-2b6a-4774-80ee-00246fb0ea9b"
},
"title": [
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
}
}
],
"properties": {
"Name": {
"title": {}
},
"Description": {
"rich_text": {}
},
"In stock": {
"checkbox": {}
},
"Meals": {
"relation": {
"database_id": "668d797c-76fa-4934-9b05-ad288df2d136",
}
},
"Number of meals": {
"rollup": {
"rollup_property_name": "Name",
"relation_property_name": "Meals",
"function": "count"
}
}
}
}'
```
#### Example response
```json JSON theme={null}
{
"object": "database",
"id": "bc1211ca-e3f1-4939-ae34-5260b16f627c",
"created_time": "2021-07-08T23:50:00.000Z",
"last_edited_time": "2021-07-08T23:50:00.000Z",
"title": [
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Grocery List",
"href": null
}
],
"properties": {
"Name": {
"id": "title",
"type": "title",
"title": {}
},
"Description": {
"id": "V}lX",
"type": "rich_text",
"rich_text": {}
},
"In stock": {
"id": "V>GQ",
"type": "checkbox",
"checkbox": {}
},
"Meals": {
"type": "relation",
"relation": {
"database_id": "668d797c-76fa-4934-9b05-ad288df2d136",
"synced_property_name": "Related to Grocery List (Meals)"
}
},
"Number of meals": {
"id": "Z\\Eh",
"type": "rollup",
"rollup": {
"rollup_property_name": "Name",
"relation_property_name": "Meals",
"rollup_property_id": "title",
"relation_property_id": "mxp^",
"function": "count"
}
},
},
"parent": {
"type": "page_id",
"page_id": "98ad959b-2b6a-4774-80ee-00246fb0ea9b"
}
}
```
## August 24, 2021
### Page icons, cover images, new block types, and improved page file properties
We have added support for linking to external image and file URLs, and many new block types, including image, embed, and file blocks.
You can now use the Notion API to:
* Retrieve and update [page](/reference/page) and [database](/reference/database) icons and cover images.
* [List](/reference/get-block-children) and [append](/reference/patch-block-children) embed, image, video, file, PDF, and bookmark blocks
* Retrieve URL for [file page properties](/reference/page-property-values#files)
* Update [file page properties](/reference/page-property-values#files)
We do not yet support uploading files to Notion through the API, however, any files already uploaded to Notion can be retrieved. You can reference the details of what is supported [here](/reference/file-object).
#### Page Icons and Cover Images
When fetching a [Page object](/reference/page) or a [Database object](/reference/database), the response will now include an `icon` and `cover` property, as shown below:
```json JSON theme={null}
{
"object": "database",
"id": "96433ad8-3fbe-460f-a007-72311c4aa804",
"cover": {
"type": "external",
"external": {
"url": "https://website.domain/images/image.png"
}
},
"icon": {
"type": "emoji",
"emoji": "🎉"
},
// ... remaining properties
}
```
The [Create a page](/reference/post-page), [Update page](/reference/patch-page), [Create a database](/reference/create-a-database), and [Update database](/reference/update-a-database) API endpoints now support the ability to set the page icon and cover image.
#### New Block Types
You can now can retrieve and add embed, image, video, file, pdf, and bookmark blocks when using [Append block children](/reference/patch-block-children) and [Retrieve block children](/reference/get-block-children).
Below is an example response from [Retrieve a page](/reference/retrieve-a-page) containing an image uploaded to Notion.
```json JSON theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "4896a9bf-ada2-4bec-8ea2-97bccf07c4ef",
"created_time": "2021-08-20T21:12:00.000Z",
"last_edited_time": "2021-08-20T21:12:00.000Z",
"has_children": false,
"type": "image",
"image": {
"caption": [],
"type": "file",
"file": {
"url": "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/64f658a7-eb31-4f98-8bea-0aa2956ec475/brocolli.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210820%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210820T211229Z&X-Amz-Expires=3600&X-Amz-Signature=e2adc496254ccc741d7ab4f3bab0de7a51b60e31a49d11fcf8702ead2ec9ec18&X-Amz-SignedHeaders=host",
"expiry_time": "2021-08-20T22:12:29.066Z"
}
}
}
],
"next_cursor": null,
"has_more": false
}
```
Third-party web applications, e.g. Typeform, Figma, etc., are retrieved and added as embed blocks.
#### File Page Properties
When retrieving file page properties, you'll now get a link to the file as well as the name.
```json JSON theme={null}
{
"object": "page",
"properties": {
"Files": {
"id": "YP~`",
"type": "files",
"files": [
{
"name": "Brocolli",
"type": "file",
"file": {
"url": "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/c32db351-d1ea-40c2-9660-820db28c44ad/brocolli.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210820%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210820T211042Z&X-Amz-Expires=3600&X-Amz-Signature=859a24c9b7153860b252fa5955829a97632650dcdc5e91c7a831a48c5efecae4&X-Amz-SignedHeaders=host",
"expiry_time": "2021-08-20T22:10:42.022Z"
}
},
{
"name": "Text File",
"type": "external",
"external": {
"url": "https://website.domain/files/doc.txt"
}
}
]
},
},
// ... remaining properties
}
```
We also support updating file page properties via [Update page](/reference/patch-page).
## August 20, 2021
### Releasing Notion-Version 2021-08-16
We're releasing Notion-Version `2021-08-16` with the following *backwards incompatible* changes:
* [Unknown Keys Will Fail Validation](#unknown-keys-will-fail-validation)
* [Rollup Property Types](#changes-to-array-rollup-property-types)
* [Append Block Children](#append-block-children-returns-a-list-of-blocks)
* [URL Safe Property IDs](#property-ids-are-now-url-safe)
* [Empty Properties Are Now Returned](#empty-database-properties-are-now-returned-as-null)
#### Unknown Keys Will Fail Validation
Previously, our endpoints used to only validate against the expected keys in both request body parameters as well as query parameters resulting in some ambiguity between incorrect behavior and invalid inputs. Going forward, to improve the developer experience we will be raising validation errors if keys that are not supported by our API are passed in to requests.
To safely migrate to `2021-08-16`, we recommend thoroughly testing your API calls against the `2021-08-16` version, to see if you get any validation errors due to this change. If you do, remove any parameters that are rejected due to unknown keys.
#### Changes to Array Rollup Property Types
Starting with the Notion-Version header `2021-08-16`, we are introducing a change to the response for rollup properties on a page which are arrays. Number and date rollups are unaffected. Specifically, the `type` of elements within an array rollup has been made consistent with property types across other API endpoints:
| Before | After |
| ---------------- | ------------------- |
| `type: "file"` | `type: "files"` |
| `type: "text"` | `type: "rich_text"` |
| `type: "person"` | `type: "people"` |
An example rollup property value for an array of rich text values, using Notion-Version `2021-08-16`:
```json JSON theme={null}
{
"properties": {
"sample text array rollup": {
"id": "NXTh",
"type": "rollup",
"rollup": {
"type": "array",
"array": [
{
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": {
"content": "hello world!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "hello world!",
"href": null
}
]
},
{
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": {
"content": "foo bar",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "foo bar",
"href": null
}
]
}
]
}
},
}
}
```
#### Append Block Children returns a list of blocks
The [Append Block Children](/reference/patch-block-children) endpoint will now return a list of the newly created [Block object](/reference/block) children.
Previously the endpoint returned the block object of the parent block. Developers can instead use the [Retrieve a block](/reference/retrieve-a-block) endpoint to get the full block object for a specified `block_id`.
This change allows developers to get `block_id`'s and additional information of the new blocks right after they're created. Note: only the first level block children are returned. To get sub-children, use the [Retrieve block children](/reference/get-block-children) endpoint.
```json Example response theme={null}
{
"object": "list",
"results": [
{
"object": "block",
"id": "9bc30ad4-9373-46a5-84ab-0a7845ee52e6",
"created_time": "2021-03-16T16:31:00.000Z",
"last_edited_time": "2021-03-16T16:32:00.000Z",
"has_children": false,
"type": "heading_2",
"heading_2": {
"text": [
{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Lacinato kale",
"href": null
}
]
}
},
{
"object": "block",
"id": "7face6fd-3ef4-4b38-b1dc-c5044988eec0",
"created_time": "2021-03-16T16:34:00.000Z",
"last_edited_time": "2021-03-16T16:36:00.000Z",
"has_children": false,
"type": "paragraph",
"paragraph": {
"text": [
{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": {
"url": "https://en.wikipedia.org/wiki/Lacinato_kale"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Lacinato kale",
"href": "https://en.wikipedia.org/wiki/Lacinato_kale"
},
{
"type": "text",
"text": {
"content": " is a variety of kale with a long tradition in Italian cuisine, especially that of Tuscany. It is also known as Tuscan kale, Italian kale, dinosaur kale, kale, flat back kale, palm tree kale, or black Tuscan palm.",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": " is a variety of kale with a long tradition in Italian cuisine, especially that of Tuscany. It is also known as Tuscan kale, Italian kale, dinosaur kale, kale, flat back kale, palm tree kale, or black Tuscan palm.",
"href": null
}
]
}
}
],
"next_cursor": null,
"has_more": false
}
```
#### Property IDs are now URL Safe
Endpoints that return property IDs as part of the response body will now return new URL safe encoded property IDs. Any request that uses property IDs (such as [Update a database](/reference/update-a-database) or [Update a page](/reference/patch-page)) should use the new URL safe ID.
This ensures all property IDs can be referenced in the URL of any new endpoints moving forward.
| Before | After |
| ------- | -------- |
| `DoS\` | `DoS%5C` |
| `title` | `title` |
| `vEKn` | `vEKn` |
#### Empty database properties are now returned as `null`
Previously, empty properties of date, email, number, and rollup types were omitted from the page response. Now, these empty properties are returned with `null` values.
```json Example page response with empty properties theme={null}
{
"object": "page",
"id": "a8b7e96d-22ce-44d8-991f-6c4535af6608",
"created_time": "2021-05-12T06:16:00.000Z",
"last_edited_time": "2021-08-12T21:36:00.000Z",
"cover": null,
"icon": null,
"parent": {
"type": "database_id",
"database_id": "70ff0393-6b40-4e76-afc4-4a26a9aa1606"
},
"archived": false,
"properties": {
"MyDate": {
"id": "GZT~",
"type": "date",
"date": null
},
"Description": {
"id": "RPld",
"type": "rich_text",
"rich_text": []
},
"Food group": {
"id": "W_iA",
"type": "select",
"select": null
},
"myRollup": {
"id": "%5Eu%3EC",
"type": "rollup",
"rollup": {
"type": "array",
"array": []
}
},
"MyRelation": {
"id": "_pL%3A",
"type": "relation",
"relation": []
},
"Price": {
"id": "dPVb",
"type": "number",
"number": null
},
"MyMultiSelect": {
"id": "%7B%40%5Co",
"type": "multi_select",
"multi_select": null
},
},
"url": "https://notion.so/a8b7e96d22ce44d8991f6c4535af6608"
}
```
## August 20, 2021
### Formula properties can now be created in databases
When [creating](/reference/create-a-database) or [updating](/reference/update-a-database) databases, you can now add `formula` property types.
#### Example request
```bash cURL theme={null}
curl --location --request POST 'https://api.notion.com/v1/databases/' \
--header 'Authorization: Bearer '"$NOTION_API_KEY"'' \
--header 'Content-Type: application/json' \
--header 'Notion-Version: 2021-05-13' \
--data '{
"parent": {
"type": "page_id",
"page_id": "98ad959b-2b6a-4774-80ee-00246fb0ea9b"
},
"title": [
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
}
}
],
"properties": {
"Name": {
"title": {}
},
"Description": {
"rich_text": {}
},
"In stock": {
"checkbox": {}
},
"Price": {
"number": {
"format": "dollar"
}
},
"Cost of next trip": {
"formula": {
"expression": "if(prop(\"In stock\"), 0, prop(\"Price\"))"
}
}
}
}'
```
#### Example response
```json JSON theme={null}
{
"object": "database",
"id": "c23f0085-a061-41c0-b8a6-cbe14d15a4de",
"created_time": "2021-08-20T16:08:00.000Z",
"last_edited_time": "2021-08-20T16:08:00.000Z",
"title": [
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Grocery List",
"href": null
}
],
"properties": {
"Cost of next trip": {
"id": "Rbq\\",
"name": "Cost of next trip",
"type": "formula",
"formula": {
"expression": "if(prop(\"In stock\"), 0, prop(\"Price\"))"
}
},
"In stock": {
"id": "\\JwC",
"name": "In stock",
"type": "checkbox",
"checkbox": {}
},
"Description": {
"id": "`}HT",
"name": "Description",
"type": "rich_text",
"rich_text": {}
},
"Price": {
"id": "u|<{",
"name": "Price",
"type": "number",
"number": {
"format": "dollar"
}
},
"Name": {
"id": "title",
"name": "Name",
"type": "title",
"title": {}
}
},
"parent": {
"type": "page_id",
"page_id": "98ad959b-2b6a-4774-80ee-00246fb0ea9b"
}
}
```
## August 11, 2021
### OAuth token response now includes workspace ID and owner info
We now return a `workspace_id` field and an `owner` in the [token response](/guides/get-started/authorization) at the very end of the OAuth authorization flow.
`workspace_id` is the ID of the workspace where the integration was authorized. As a reminder, this is **not** intended to be unique across tokens; in future iterations of our authorization flow users may be able to authorize your integration multiple times in the same workspace.
`owner` contains information about who can view and share the integration. Because all integrations today can be viewed and shared by all members in the space, `owner` is just an object that looks like `{ "workspace": true }` for now.
To summarize, the OAuth token response now looks like this:
| Field | Type | Description | Not null |
| ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------- | -------- |
| `"access_token"` | `string` | An access token used to authorize requests to the Notion API. | ✅ |
| `"workspace_id"` | `string` | The ID of the workspace where this authorization took place. | ✅ |
| `"workspace_name"` | `string` | A human-readable name which can be used to display this authorization in UI. | |
| `"workspace_icon"` | `string` | A URL to an image which can be used to display this authorization in UI. | |
| `"bot_id"` | `string` | An identifier for this authorization. | ✅ |
| `"owner"` | `object` | An object containing information about who can view and share this integration. Always `{ "workspace": true }` for now. | ✅ |
## August 11, 2021
### Update existing databases with PATCH /v1/databases
You can now use the Notion API to [update databases](/reference/update-a-database)!
Supported updates are:
* renaming the database
* adding and removing properties
* renaming properties
* updating property types.
Note that updating the `name` and `color` select and multi select options is not supported.
#### Example request
```bash cURL expandable theme={null}
curl --location --request PATCH 'https://api.notion.com/v1/databases/668d797c-76fa-4934-9b05-ad288df2d136' \
--header 'Authorization: Bearer '"$NOTION_API_KEY"'' \
--header 'Content-Type: application/json' \
--header 'Notion-Version: 2021-07-27' \
--data '{
"title": [
{
"text": {
"content": "Today'\''s grocery list"
}
}
],
"properties": {
"+1": null,
"Photo": {
"url": {}
},
"Store availability": {
"multi_select": {
"options": [
{
"name": "Duc Loi Market"
},
{
"name": "Rainbow Grocery"
},
{
"name": "Gus'\''s Community Market"
},
{
"name": "The Good Life Grocery",
"color": "orange"
}
]
}
}
}
}'
```
#### Example response
```json JSON expandable theme={null}
{
"object": "database",
"id": "668d797c-76fa-4934-9b05-ad288df2d136",
"created_time": "2020-03-17T19:10:00.000Z",
"last_edited_time": "2021-08-11T17:26:00.000Z",
"parent": {
"type": "page_id",
"page_id": "48f8fee9-cd79-4180-bc2f-ec0398253067"
},
"title": [
{
"type": "text",
"text": {
"content": "Today'\''s grocery list",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Today'\''s grocery list",
"href": null
}
],
"properties": {
"Name": {
"id": "title",
"type": "title",
"title": {}
},
"Description": {
"id": "J@cS",
"type": "rich_text",
"rich_text": {}
},
"In stock": {
"id": "{xY`",
"type": "checkbox",
"checkbox": {}
},
"Food group": {
"id": "TJmr",
"type": "select",
"select": {
"options": [
{
"id": "96eb622f-4b88-4283-919d-ece2fbed3841",
"name": "🥦Vegetable",
"color": "green"
},
{
"id": "bb443819-81dc-46fb-882d-ebee6e22c432",
"name": "🍎Fruit",
"color": "red"
},
{
"id": "7da9d1b9-8685-472e-9da3-3af57bdb221e",
"name": "💪Protein",
"color": "yellow"
}
]
}
},
"Price": {
"id": "cU^N",
"type": "number",
"number": {
"format": "dollar"
}
},
"Cost of next trip": {
"id": "p:sC",
"type": "formula",
"formula": {
"value": "if(prop(\"In stock\"), 0, prop(\"Price\"))"
}
},
"Last ordered": {
"id": "]\\R[",
"type": "date",
"date": {}
},
"Meals": {
"type": "relation",
"relation": {
"database": "668d797c-76fa-4934-9b05-ad288df2d136",
"synced_property_name": null
}
},
"Number of meals": {
"id": "Z\\Eh",
"type": "rollup",
"rollup": {
"rollup_property_name": "Name",
"relation_property_name": "Meals",
"rollup_property_id": "title",
"relation_property_id": "mxp^",
"function": "count"
}
},
"Store availability": {
"type": "multi_select",
"multi_select": {
"options": [
[
{
"id": "d209b920-212c-4040-9d4a-bdf349dd8b2a",
"name": "Duc Loi Market",
"color": "blue"
},
{
"id": "70104074-0f91-467b-9787-00d59e6e1e41",
"name": "Rainbow Grocery",
"color": "gray"
},
{
"id": "6c3867c5-d542-4f84-b6e9-a420c43094e7",
"name": "Gus's Community Market",
"color": "yellow"
},
{
"id": "a62fbb5f-fed4-44a4-8cac-cba5f518c1a1",
"name": "Good life grocery",
"color": "orange"
}
]
]
}
}
"Photo": {
"id": "aTIT",
"type": "url",
"url": {}
}
}
}
```
### Retrieve and update blocks with GET and PATCH /v1/blocks/:id
You can now retrieve and update block objects with the Notion API! The `PATCH` endpoint currently supports updating `paragraph`, `heading_1`, `heading_2`, `heading_3`, `bulleted_list_item`, `numbered_list_item`, `toggle` and `to_do` blocks.
#### Retrieve a Block
The [Retrieve a Block](/reference/retrieve-a-block) endpoint returns a [Block Object](/reference/block).
```bash Example GET request theme={null}
curl 'https://api.notion.com/v1/blocks/9bc30ad4-9373-46a5-84ab-0a7845ee52e6' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H 'Notion-Version: 2021-05-13'
```
```json Example response expandable theme={null}
{
"object": "block",
"id": "9bc30ad4-9373-46a5-84ab-0a7845ee52e6",
"created_time": "2021-03-16T16:31:00.000Z",
"last_edited_time": "2021-03-16T16:32:00.000Z",
"has_children": false,
"type": "to_do",
"to_do": {
"text": [
{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Lacinato kale",
"href": null
}
],
"checked": false
}
}
```
#### Update a Block
The new `PATCH` `/v1/blocks/:id` endpoint supports updating block content (the properties within the block type object) and returns the updated [Block Object](/reference/block), same as the `GET` endpoint shown above. See the [Update a Block](/reference/update-a-block) documentation for more detail.
```bash Example PATCH request theme={null}
curl https://api.notion.com/v1/blocks/9bc30ad4-9373-46a5-84ab-0a7845ee52e6 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2021-05-13" \
-X PATCH \
--data '{
"to_do": {
"text": [{
"text": { "content": "Lacinato kale" }
}],
"checked": false
}
}'
```
### Number properties now support more currency formats
The number property type in databases now supports additional currency options.
The new options are:
* "hong\_kong\_dollar"
* "new\_zealand\_dollar"
* "krona"
* "norwegian\_krone"
* "mexican\_peso"
* "rand"
* "new\_taiwan\_dollar"
* "danish\_krone"
* "zloty"
* "baht"
* "forint"
* "koruna"
* "shekel"
* "chilean\_peso"
* "philippine\_peso"
* "dirham"
* "colombian\_peso"
* "riyal"
* "ringgit"
* "leu"
This impacts the [number configuration of databases](/reference/property-schema-object#number-configuration).
### OAuth improvements
We've made improvements to the OAuth flow to make it easier to use.
**We now show the page picker on reauthorization.** Just like before, the user who initially authorized an integration can reauthorize by going through OAuth a second time. The page picker step will remember which pages have already been shared with the integration, if any, and let users share or un-share additional pages.
**Users can search for pages to share with an integration.** Previously, users could only select pages at the top level of the Workspace, Shared section, or Private pages section to share with an integration, but we've added a search bar so users can search for and select any page in their workspace.
We also updated the page picker to only show pages for which the user has Full Access permission. Previously, the page picker show any pages for which the user had at least Can View permission, but would show an error when they tried to give the permission access to those pages.
Other OAuth behavior has not changed: only admins can go through OAuth, and only the original person originally added an integration via OAuth can go through the flow again.
### Database property objects now include the property name
[Database property objects](/reference/property-schema-object#database-properties) now include the field `name` with the property name as it appears in Notion.
### Rollup property functions now include show\_original
The `function` `show_original` has now been added to [rollup database property objects](/reference/property-schema-object#rollup-configuration). This fixes a bug where rollup properties were omitted if the calculation was "Show Original".
### Create new databases with POST /v1/databases
You can now use the Notion API to [create a database](/reference/create-a-database) as a subpage of an existing page. Currently supported property types are `"title"`, `"rich_text"`, `"number"`, `"select"`, `"multi_select"`, `"date"`, `"people"`, `"files"`, `"checkbox"`, `"url"`, `"email"`, `"phone_number"`, `"created_time"`, `"created_by"`, `"last_edited_time"`, `"last_edited_by"`.
#### Example request
```bash cURL expandable theme={null}
curl --location --request POST 'https://api.notion.com/v1/databases/' \
--header 'Authorization: Bearer '"$NOTION_API_KEY"'' \
--header 'Content-Type: application/json' \
--header 'Notion-Version: 2021-05-13' \
--data '{
"parent": {
"type": "page_id",
"page_id": "98ad959b-2b6a-4774-80ee-00246fb0ea9b"
},
"title": [
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
}
}
],
"properties": {
"Name": {
"title": {}
},
"Description": {
"rich_text": {}
},
"In stock": {
"checkbox": {}
},
"Food group": {
"select": {
"options": [
{
"name": "🥦Vegetable",
"color": "green"
},
{
"name": "🍎Fruit",
"color": "red"
},
{
"name": "💪Protein",
"color": "yellow"
}
]
}
},
"Price": {
"number": {
"format": "dollar"
}
},
"Last ordered": {
"date": {}
},
"Store availability": {
"type": "multi_select",
"multi_select": {
"options": [
{
"name": "Duc Loi Market",
"color": "blue"
},
{
"name": "Rainbow Grocery",
"color": "gray"
},
{
"name": "Nijiya Market",
"color": "purple"
},
{
"name": "Gus'\''s Community Market",
"color": "yellow"
}
]
}
},
"+1": {
"people": {}
},
"Photo": {
"files": {}
}
}
}'
```
#### Example response
```json JSON expandable theme={null}
{
"object": "database",
"id": "bc1211ca-e3f1-4939-ae34-5260b16f627c",
"created_time": "2021-07-08T23:50:00.000Z",
"last_edited_time": "2021-07-08T23:50:00.000Z",
"title": [
{
"type": "text",
"text": {
"content": "Grocery List",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Grocery List",
"href": null
}
],
"properties": {
"+1": {
"id": "PNEQ",
"type": "people",
"people": {}
},
"In stock": {
"id": "V>GQ",
"type": "checkbox",
"checkbox": {}
},
"Price": {
"id": "V@]u",
"type": "number",
"number": {
"format": "dollar"
}
},
"Description": {
"id": "V}lX",
"type": "rich_text",
"rich_text": {}
},
"Last ordered": {
"id": "eVnV",
"type": "date",
"date": {}
},
"Store availability": {
"id": "s}Kq",
"type": "multi_select",
"multi_select": {
"options": [
{
"id": "cb79b393-d1c1-4528-b517-c450859de766",
"name": "Duc Loi Market",
"color": "blue"
},
{
"id": "58aae162-75d4-403b-a793-3bc7308e4cd2",
"name": "Rainbow Grocery",
"color": "gray"
},
{
"id": "22d0f199-babc-44ff-bd80-a9eae3e3fcbf",
"name": "Nijiya Market",
"color": "purple"
},
{
"id": "0d069987-ffb0-4347-bde2-8e4068003dbc",
"name": "Gus's Community Market",
"color": "yellow"
}
]
}
},
"Photo": {
"id": "yfiK",
"type": "files",
"files": {}
},
"Food group": {
"id": "|JKd",
"type": "select",
"select": {
"options": [
{
"id": "6d4523fa-88cb-4ffd-9364-1e39d0f4e566",
"name": "🥦Vegetable",
"color": "green"
},
{
"id": "268d7e75-de8f-4c4b-8b9d-de0f97021833",
"name": "🍎Fruit",
"color": "red"
},
{
"id": "1b234a00-dc97-489c-b987-829264cfdfef",
"name": "💪Protein",
"color": "yellow"
}
]
}
},
"Name": {
"id": "title",
"type": "title",
"title": {}
}
},
"parent": {
"type": "page_id",
"page_id": "98ad959b-2b6a-4774-80ee-00246fb0ea9b"
}
}
```
### User mentions can only be of people
To be consistent with the Notion application, only users of type "people" can be mentioned in rich text objects or in people properties of databases. Trying to include users of type "bot" will return a validation error. Existing mentions of bot users is unaffected.
### Page objects now contain url
[Page objects](/reference/page) now return the web address of the page in the `url` key.
```json JSON expandable theme={null}
{
"object": "page",
"id": "251d2b5f-268c-4de2-afe9-c71ff92ca95c",
"created_time": "2020-03-17T19:10:04.968Z",
"last_edited_time": "2020-03-17T21:49:37.913Z",
"parent": {
"type": "database_id",
"database_id": "48f8fee9-cd79-4180-bc2f-ec0398253067"
},
"archived": false,
"url": "https://www.notion.so/Avocado-251d2b5f268c4de2afe9c71ff92ca95c",
"properties": {
"Name": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Avocado",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Avocado",
"href": null
}
]
}
}
}
```
This impacts endpoints that return page object: the [pages](/reference/page) endpoints and [query database](/reference/post-database-query) endpoint.
### Last edited and created time properties are now rounded to the nearest minute
Starting July 1st, the `last_edited_time` and `created_time` properties will be rounded down to the closest minute for `page`, `database` , and `block` objects. Previously, this behavior was inconsistent with some times being rounded and others not.
### Database objects now return parent
Database objects now return a [parent property](/reference/parent-object#database-parent). Databases can have pages or workspaces as parents.
```json JSON theme={null}
{
"parent": {
"type": "page_id",
"page_id": "b8595b75-abd1-4cad-8dfe-f935a8ef57cb"
}
```
### Other Improvements and Fixes
* Inline database mentions are now included in [rich\_text mention](/reference/rich-text#database-mention-type-object) responses.
* When an integration does not have access to a [page or database mention](/reference/rich-text#page-mention-type-object), we will no longer completely omit the mention. The mention will be returned with just the ID but without detailed information (title will appear as "Untitled" and annotations will be default).
* When integrations are added to pages inside collections they can now always update page properties, even when the integration does not have access to the parent database. However, integrations will not be able to add new [select](/reference/page-property-values#select) or [multi-select](/reference/page-property-values#multi-select) properties through the create or update page endpoints without the ability to edit the database parent.
### Select values can now be dynamically created via Create and Update Page endpoints + other updates since public beta launch
You can now dynamically create new options for [Select](/reference/property-schema-object#select-configuration) or [Multi-Select properties](/reference/property-schema-object#multi-select-configuration) when using the [Create Page](/reference/post-page) and [Update Page](/reference/patch-page) endpoints. When specifying an option that does not exist in the database schema already, the option will now be created and the database schema updated accordingly.
```json JSON theme={null}
{
"properties": {
"Food group": {
"multi_select": [{"name": "Vegetable"},{"name": "Fruit"}]
}
}
}
```
In the above [property values](/reference/page-property-values) example: Previously, if either "Vegetable" or "Fruit" did not already exist as an option in the database schema, an error would be returned that these are not valid Select options. Now, these options will be created dynamically.
**Bug Fixes**
* The title property of a page can be set, and a page can be archived or un-archived, even when the page does not belong to a database.
* [Retrieving pages](/reference/post-page) that are shared with an integration, but where the page's parent is not shared, no longer erroneously returns a 404.
**Other Changes**
* [Search endpoint](/reference/post-search) now returns untitled pages.
* Applies to [version 2021-05-13](/reference/versioning) and later only: The [Query Database](/reference/post-database-query) endpoint no longer accepts query parameters – these should be sent as body parameters.
### "Notion-Version" header will be required starting June 1, 2021
The Notion API has recommended using an explicit version to every HTTP request, using the `Notion-Version` header. **For integrations created after June 1, 2021 an explicit version on every request will become required**. After July 1, 2021, integrations created before June 1, 2021 will also have the same requirement. Today, the most recent version is `"2021-05-13"`.
#### Is my integration affected? What should I do to update?
This requirement will not break your existing integration; however, we will start enforcing this requirement for all API requests on July 1st. Starting July 1st, if you don't send the Notion-Version header with your Notion API calls, you will get a [`"missing_version"` error](/reference/errors). Learn more about how the Notion API [handles versioning](/reference/versioning).
If you've been using examples copied from documentation or example code since the public beta, including using the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js), your existing code should continue to work as expected.
Otherwise, please make one of the two following changes before July 1:
1. Add `Notion-Version: 2021-05-11` in the header when making requests (no other code change is needed).
2. **Recommended**: Add `Notion-Version: 2021-05-13` in the header when making requests. Making this change will move you to our newest version which includes the following breaking change.
#### Breaking changes in version `2021-05-13`
The `type` of [property value objects](/reference/page-property-values) for rich text properties has changed from `"text"` to `"rich_text"`.
When [creating pages](/reference/post-page) and [updating page properties](/reference/patch-page), update page property values that are rich text to use the key `rich_text` instead of `text`. Similarly when [retrieving a page](/reference/post-page), rich text properties will be returned with the `type` `"rich_text"` instead of `"text"`.
This change helps distinguish between the property type, and the inner text values of [rich text object](/reference/rich-text), which have the key `text`.
To illustrate this change, here is an example of how the [page object](/reference/page)'s `properties` appear before and after:
```js JavaScript theme={null}
// Before (in unversioned requests and responses)
{
"object": "page",
"properties": {
"Description": {
"type": "text",
"text": [
{
"type": "text",
"text": { "content": "Hello World" }
}
]
}
}
/* remaining details omitted */
}
```
```js JavaScript theme={null}
// After (in requests and responses with version 2021-05-13)
{
"object": "page",
"properties": {
"Description": {
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": { "content": "Hello World" }
}
]
}
}
/* remaining details omitted */
}
```
### Initial users may reauthorize a public integration using OAuth
Public integrations use OAuth to [request authorization from a user](/guides/get-started/authorization) before being added to a Notion workspace. Previously, once an integration was added to a workspace, no users were able to reauthorize the same integration in that workspace. This change allows the user who initially added the integration to complete the authorization flow more than once. This improvement helps integrations avoid a potential dead end in user flows.
Integrations do not need to make any updates to take advantage of this new capability. An integration may simply link or redirect a user to the authorization URL, the same as the first authorization. During reauthorization the user will not see the page picker. The access token received at the end of reauthorization will be the same as the initial access token. This capability is available immediately.
Please be aware that other previous limitations still exist. Only users with admin access level in a workspace can add an integration. Integrations can only be added to a workspace by one user.
### Hello world, the Notion API is now in public beta
The Notion API is now available for all developers to explore and build upon. Integrations built on the API are available to all Notion users, on free or paid plans.
In this public beta release, you'll find many of the fundamental parts of Notion: reading and writing to pages, working with users, and the deep and powerful world of Notion databases. The API itself offers foundational features such as authorization, pagination, limits, and more. This is enough to build many interesting integrations we've heard Notion users are excited to use. We're excited to see what you'll build for all of us. [→ Get started](/guides/get-started/overview)
Our goal is to establish that the Notion API is robust, easy to use, and trustworthy. In public beta, we’ll continue to add new features and making significant changes based on your feedback. Once the most important improvements are included, the API will transition from public beta to general availability. you’ll have everything you need to build integrations teams and businesses can depend on.
A special thanks to the all developers who experimented, explored, and shared their ideas with us - both in the private beta and those following along.
### Public integration type extends access to multiple workspaces using OAuth
The Notion API has added a new integration type: Public OAuth integrations. If you're building a product or service for Notion users outside your own team - public integrations are for developers like you.
In order to create and configure an integration, its type, name, avatar, and other related settings, the My integrations page is now available.
Public integrations use OAuth to request permission to access pages and databases in new workspaces. Once the user accepts, the integration can receive a separate access token for resources in the user's workspace.
Existing integrations are now known as internal integrations. We no longer call the bearer token you previously used an API Key. It's now known as an integration token. You can keep your bearer token around - it will continue to work just the same.
Learn how to implement these changes in the [authorization guide](/guides/get-started/authorization).
### Search is now available in the API
Using the new [search endpoint](/reference/post-search), you can query all pages and databases users have shared with your integration.
The query you provide filters results by matching against the page title. The results also include matches against subpages of pages users have shared with your integration. This endpoint can be helpful when onboarding a new user and trying to find the page they just shared with your integration.
We recommend transitioning away from using the [list databases endpoint](/reference/get-databases). The search endpoint provides all the same functionality - and more.
# Postman workspace
Source: https://developers.notion.com/guides/resources/notion-s-api-workspace
# Changelog
Source: https://developers.notion.com/page/changelog
### Truncation metadata on `notion-fetch`
The [`notion-fetch`](/guides/mcp/mcp-supported-tools) MCP tool response now includes `truncated`, `unknown_block_ids`, and `unknown_block_count` when a page is large enough that some subtrees could not be loaded. `unknown_block_ids` lists up to 50 omitted subtree root IDs, and `unknown_block_count` reports the total number of omitted subtree roots. Pass a returned ID back to `notion-fetch` to retrieve that subtree directly; treat an `object_not_found` error on retry as a signal that the caller does not have access to the subtree.
### `unsupported` formula and rollup property values
The API can now return formula and rollup [page property values](/reference/page-property-values) and [property item values](/reference/property-item-object) with `type` set to `"unsupported"` and an empty `unsupported` object. This happens when a value depends on too many related pages or nested formulas and rollups. The response doesn't include a partial value. Treat the property as unavailable. Rollups still include the `function` field. To make the value available, reduce the number of related pages or simplify the nested formulas and rollups.
### JavaScript SDK 5.24.0
We released [`@notionhq/client` v5.24.0](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.24.0). It adds `client.blocks.meetingNotes.create()` for [creating a meeting note](/reference/create-meeting-note), typed `unsupported` formula and rollup values, and `APIErrorCode.InvalidBeta` for handling `invalid_beta` responses.
### Notion MCP supports MCP protocol version 2026-07-28
The Notion MCP Streamable HTTP endpoint at [`https://mcp.notion.com/mcp`](https://mcp.notion.com/mcp) now supports [MCP protocol version 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28). Existing MCP clients that negotiate the earlier 2025-era protocol on the same endpoint continue to work unchanged, so no client updates are required.
### Clearer per-tool access status
The `current_tool_access` status previously called `limited_free_trial` is now `available_with_limit`. The new name makes clear that a tool is available until the usage limit included with the workspace's plan is reached. This is a naming and description update only; it does not change tool availability or usage limits.
### Fetch documentation resources with the fetch tool
The Notion MCP `fetch` tool now accepts `notion://docs/*` URIs (for example, `notion://docs/enhanced-markdown-spec`) and returns the same content as the MCP resource of the same URI. This gives MCP clients that cannot read MCP resources a way to load the specs referenced in tool descriptions.
### Per-tool access map in `notion-fetch` `self`
The [`notion-fetch`](/guides/mcp/mcp-supported-tools) MCP tool's `self` response now includes a `current_tool_access` map, so clients can tell before making a call which tools will actually run on the connected workspace's plan and which would only return an upgrade prompt. Each entry's `status` is `available`, `limited_free_trial` (calls succeed via a free or metered trial allowance), `upgrade_required` (the entry also carries an `upgrade_url` into the workspace's upgrade flow), or `not_enabled`. See [Integrating your own MCP client](/guides/mcp/build-mcp-client#identify-the-connected-workspace).
### New identity fields in OAuth token responses
[User objects](/reference/user) now include an `email_verified` boolean next to `person.email`, indicating whether Notion has verified that email address. The field appears anywhere person emails do, including the `owner` in [OAuth token responses](/guides/get-started/authorization) and [List all users](/reference/get-users) results.
Notion MCP token responses now include top-level `user_id`, `workspace_id`, and `email_domain` fields on successful authorization-code exchanges, so MCP clients can associate a connection with a Notion user and workspace without an extra call. See [Integrating your own MCP client](/guides/mcp/build-mcp-client#step-6-exchange-authorization-code-for-tokens).
New response fields like these are [backwards-compatible additions](/reference/versioning#what-we-consider-backwards-compatible) and appear on every API version. Parse responses leniently: ignore fields you don't recognize rather than rejecting them.
### Notion app links in API responses use the new app domain
As part of Notion's move from `notion.so` to `notion.com`, the links Notion generates for its own records changed in early June 2026: the `url` values returned for [pages](/reference/page), [databases](/reference/database), and [data sources](/reference/data-source), and the `href` values for page and database [mentions](/reference/rich-text), now point at the Notion app domain with a page path prefix, `https://app.notion.com/p/{page-id}`, instead of `https://www.notion.so/{page-id}`. Existing `notion.so` links continue to open correctly.
These values are links for people to open in Notion, not stable identifiers: their domain and path format may change again. To reference a record, use its `id` field rather than parsing the URL, and use a page's [`public_url`](/reference/page) to link to its published site. Links authored by users, such as `link.url` in rich text and URL property values, and links to sites published on `notion.site` or custom domains are unchanged.
### Search the trash and query archived pages
[Search](/reference/post-search) accepts a new `filter.in_trash` option to list trashed pages and data sources (databases on API versions before `2025-09-03`). [Query a data source](/reference/query-a-data-source#archived-pages) accepts a top-level `is_archived` body parameter to return archived pages instead of the default non-archived set.
**SDK support**: `@notionhq/client` [`v5.23.2`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.23.2) adds `filter.in_trash` support to `client.search()`. [`v5.23.1`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.23.1) exports rich text annotation types and fixes pagination helper type compatibility with endpoint methods under `strictNullChecks`.
### Longer-lived Notion MCP access tokens
Notion MCP access tokens now last about eight hours, up from one hour. Clients
must continue to rely on the token response's `expires_in` value, but the longer
lifetime reduces refresh frequency and makes connections more resilient to
client-side refresh failures.
We released [`v5.23.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.23.0) of `@notionhq/client`, our SDK for JavaScript and TypeScript. Here's what's new:
### Verify webhook signatures with one call
The new `verifyWebhookSignature()` helper confirms that a [webhook](/reference/webhooks) event really came from Notion, so you no longer need to hand-write the HMAC check. Pass the raw request body, the `X-Notion-Signature` header, and your subscription's `verification_token`; the helper compares signatures in constant time and returns `false` instead of throwing on malformed input. It works without configuration in Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge Functions, and browsers. A companion `signWebhookPayload()` generates signatures for testing your handler.
### Read every row in a large data source
A single [data source query](/reference/query-a-data-source) returns at most 10,000 results, so plain pagination can silently miss rows. The new `iterateAllDataSourceRows()` and `collectAllDataSourceRows()` helpers page past the limit using the windowing approach from the [Query large data sources](/guides/data-apis/query-large-data-sources) guide, de-duplicating rows along the way. Stream rows with the iterator, or collect them into an array when the full result fits in memory.
### Start and poll async page writes
`notion.asyncTasks.retrieve({ task_id })` adds typed support for the [Retrieve an async task](/reference/retrieve-async-task) endpoint, and the page create and markdown update methods now accept `allow_async: true`. Together they let you [run large markdown writes asynchronously](/guides/data-apis/working-with-markdown-content#running-large-markdown-writes-asynchronously): start the write without holding a request open, then poll until the task succeeds or fails.
### Reliability and fixes
* The client now automatically retries `service_overload` (HTTP 529) responses, respecting the `Retry-After` header as described in [Request limits](/reference/request-limits). Thanks to @RyanBillard for contributing this.
* The relevance sort in search parameters now has the correct type, `{ property: "relevance" }`.
* Pagination parameters accept `start_cursor: null`, so you can pass a response's `next_cursor` straight through without a null check.
### HTML blocks via the API
You can now create [HTML blocks](/reference/block#html-blocks) with the API. Upload an `.html` file with the [File Upload API](/reference/file-upload) and attach it to an embed block via `embed.file_upload` when [appending block children](/reference/patch-block-children), [creating a page](/reference/post-page), or [updating a block](/reference/update-a-block). The Notion app renders the file's contents interactively in a sandboxed iframe — the same HTML block the app creates with the `/html` command and that agents create through [Notion MCP](/guides/mcp/mcp-supported-tools).
### Choose an expiration when creating a personal access token
When creating a [personal access token](/guides/get-started/personal-access-tokens) in the [Developer portal](https://www.notion.so/developers), you can now pick an **Expiration** of 7 days, 30 days, 90 days, 180 days, or 1 year. The default stays at 1 year, matching the previous behavior. The create dialog previews the exact expiration date, and the reveal step shows the same date next to the token value.
The workspace admin view under **Settings & members → Connections** now also surfaces an **Expired** status and filter for PATs whose expiration has passed. Expired tokens stop authenticating and return an `unauthorized` error, and can still be revoked from the admin view or the Developer portal.
### Icon names and database icons
When setting a native icon, `name` now also accepts the icon picker name, so values like `"token"` and `"star circle"` can set the same Notion icon.
`databases.retrieve` now returns the icon set in the Notion UI, matching the icon surfaced by `dataSources.retrieve`.
### Async page markdown writes
You can now opt into async responses for large page markdown create and update requests. Set `allow_async: true` when creating a page with `POST /v1/pages` and the `markdown` body parameter, or when updating page content with `PATCH /v1/pages/:page_id/markdown`. Notion returns an `async_task` handle with `status_url` and `poll_after_seconds`, which you can poll until the task succeeds or fails.
Notion MCP also supports async page create and update flows through `allow_async: true` on `notion-create-pages` and `notion-update-page`, plus the `notion-get-async-task` polling tool. See [Working with markdown content](/guides/data-apis/working-with-markdown-content#running-large-markdown-writes-asynchronously) for examples.
### Get workspace and user identity with `notion-fetch`
The [`notion-fetch`](/guides/mcp/mcp-supported-tools) MCP tool now accepts the special id `self`, returning the connected workspace and user identity instead of an entity. The response includes a `self` object with the workspace's ID and name and the authenticated user's ID, name, type, and email, letting MCP clients label a connection after OAuth without the public REST API. See [Integrating your own MCP client](/guides/mcp/build-mcp-client#identify-the-connected-workspace).
### Your AI assistant now has a complete, consistent view of Notion
We've expanded which Notion features are available when using AI assistants like Claude, ChatGPT, or any third-party agent connected to Notion via MCP.
**Expanded access for Business + Notion AI plans**
Teams on a Business plan with Notion AI can now query a single database or view directly from their AI assistant. This previously required Enterprise + Notion AI. Querying across multiple databases in a single query still requires Enterprise + Notion AI.
**Your assistant always knows what's possible**
Previously, if a Notion feature wasn't included in your plan, your AI assistant simply didn't know it existed. This sometimes led to bad outcomes. For example, your assistant might repeatedly try to search for database properties, unaware that the right tool simply wasn't visible to it.
Now your assistant has a complete picture of what Notion can do. If a feature requires a higher plan, it will say so and point you toward an upgrade rather than silently attempting the wrong approach.
### Status option groups
Status property option objects now accept an optional `group` field when creating or updating a database or data source schema. Use `group` to assign a custom status option to `To-do`, `In progress`, or `Complete`. When `group` is omitted on update, existing options keep their current group, and new options use `To-do` when present or the first existing group otherwise.
### Workspace-level rate limits
The Notion API now applies a rate limit per workspace, in addition to the existing [per-connection limit](/reference/request-limits). This limit is shared across all of a workspace's connections and scaled to the workspace's plan, so requests can be rate limited even when a single connection is within the per-connection limit. As with other rate limits, respect the `Retry-After` header on HTTP 429 responses. See [Request limits](/reference/request-limits).
### Bots in people properties and user mentions
Bots that appear as [user objects](/reference/user) in API responses can now be assigned to `people` [page property values](/reference/page-property-values#people) and referenced in `user` [rich text mentions](/reference/rich-text#user-mention-type-object). You can set a `people` property when you [create a page](/reference/post-page) or [update page properties](/reference/patch-page). Previously these writes returned a `validation_error`, even though the same bots were already returned when reading those fields. Some bots never appear as user objects, including integrations Notion uses internally to power features like database automations and custom agents. Assigning one of those still returns a `validation_error`.
### Unique access tokens per OAuth authorization
New public connections now mint a fresh `access_token` and `refresh_token` for each successful OAuth authorization instead of returning the existing active token. Existing connections keep their previous behavior. Store the token pair from every successful response — including re-authorizations of the same connection — as described in the [Authorization guide](/guides/get-started/authorization#step-5-the-connection-stores-the-access_token-and-refresh_token-for-future-requests).
### Markdown page insertion positions
The [Update page markdown](/reference/update-page-markdown) endpoint now supports `insert_content.position`, letting integrations prepend markdown to the start of a page or explicitly append it to the end without rewriting the full page. See [Working with markdown content](/guides/data-apis/working-with-markdown-content#legacy-commands) for examples.
### Developer portal and personal access tokens
The new [Developer portal](https://www.notion.so/developers) is now available as a single place to manage developer tools for Notion, including connections, Workers, and personal access tokens.
[Personal access tokens](/guides/get-started/personal-access-tokens) (PATs) are user-scoped tokens for scripts, CLI workflows, Workers, and trusted tools that should act with one Notion user's permissions. PATs can be granted Notion API access, Workers access, or both.
Workspace admins can now view and revoke PATs created in their workspace. On supported plans, admins can also configure who may create PATs with Notion API access. Defaults vary by plan: Free workspaces default to workspace owners only, Plus workspaces default to all workspace members, Business workspaces default to workspace owners only, and Enterprise workspaces default to workspace owners and selected groups.
### Query meeting notes endpoint
The new [Query meeting notes](/reference/query-meeting-notes) endpoint (`POST /v1/blocks/meeting_notes/query`) returns AI meeting notes for the integration's user with optional filter, sort, and limit. The `attendees` alias is normalized server-side so filters round-trip cleanly.
### `agent_id` parent type
Pages and blocks parented by an agent now serialize their `parent` as `{ "type": "agent_id", "agent_id": "..." }` instead of being rejected or rewritten. See [Parent object](/reference/parent-object) for the full list of parent types.
**SDK support**: `@notionhq/client` [`v5.21.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.21.0) adds typed support for `notion.blocks.meetingNotes.query()` and the `agent_id` parent variant.
Improvements to pagination reliability for the [Query a data source](/reference/post-database-query) endpoint:
* Pagination cursors now embed a session identifier, eliminating intermittent `400 validation_error` ("The start\_cursor provided is invalid") errors that could occur when multiple pagination sessions for the same query overlapped.
* The `start_cursor` parameter now accepts opaque string values in addition to UUIDs. Existing UUID-based cursors continue to work. As documented in our [versioning policy](/reference/versioning), cursors should always be treated as opaque — pass `next_cursor` values back as `start_cursor` without parsing or validating their format.
### Data source and view query pagination limit
The [Query a data source](/reference/query-a-data-source), [Create a view query](/reference/create-view-query), and [Get view query results](/reference/get-view-query-results) endpoints now enforce a maximum pagination depth of 10,000 results per query. When a query matches more rows than this limit, the response includes a new `request_status` field:
```json theme={null}
{
"request_status": {
"type": "incomplete",
"incomplete_reason": "query_result_limit_reached"
}
}
```
Integrations that polled these endpoints to iterate through all matching rows in a large data source should check for `request_status.type === "incomplete"` and adapt accordingly. The limit improves reliability for all API users by bounding the server-side resources consumed by each query.
If your integration needs to process all pages in a large data source, we recommend:
* Using [data source filters](/reference/filter-data-source-entries) or narrowing the [view's filter/sort configuration](/reference/update-a-view) to reduce the result set (for example, filter by `last_edited_time` to only fetch recently changed pages).
* Setting up [integration webhooks](/reference/webhooks) for incremental sync instead of polling the full data source on a schedule.
* Dividing large data sources into multiple smaller ones.
**SDK support**: `@notionhq/client` [`v5.20.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.20.0) adds typed support for the `request_status` field on affected list responses.
### Update and delete comment endpoints
The [Update a comment](/reference/update-a-comment) (`PATCH /v1/comments/:comment_id`) and [Delete a comment](/reference/delete-a-comment) (`DELETE /v1/comments/:comment_id`) endpoints are now generally available. Non-DLP integrations can only modify or delete comments they created.
### Multi-value filters for select, status, and multi\_select properties
[Database](/reference/post-database-query-filter) and [data source](/reference/filter-data-source-entries) filters now accept an array of values for `equals` / `does_not_equal` on select and status properties, and for `contains` / `does_not_contain` on multi\_select properties, matching the multi-value conditions available in the Notion UI. The same schema is used by [view filters and quick filters](/guides/data-apis/working-with-views#quick-filters). Person filters set via the API also now round-trip cleanly on read without extra combinator nesting.
### Notion MCP improvements
* The [`search`](/guides/mcp/mcp) tool no longer drops Slack DMs and private channel results when the connected workspace has the Slack integration enabled.
* The [`fetch`](/guides/mcp/mcp) tool now accepts any first-party Notion domain for the current environment (both `notion.so` and `notion.com`), fixing cases where pasted links fell through as generic webpages.
* Page resources returned by the `fetch` tool now include `is_archived` so agents can tell when a page is in the trash.
* The enhanced Markdown guidance the MCP presents to LLMs now documents ` ` as the correct way to break lines inside inline code, preventing retry loops when agents write multi-line inline code via `update_page`.
* The Notion MCP OAuth server adds [Client ID Metadata Document (CIMD)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document) support per [MCP spec 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25), letting clients use an HTTPS URL as their `client_id` instead of going through Dynamic Client Registration.
### Comment markdown formatting clarification
The [Create a comment](/reference/create-a-comment) and [Update a comment](/reference/update-a-comment) references now explicitly document that the `markdown` body parameter supports inline formatting only — fenced code blocks, headings, lists, tables, and blockquotes do not render as structured blocks in comments.
**SDK support**: `@notionhq/client` [`v5.18.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.18.0) adds typed support for multi-value select, status, and multi\_select filters. [`v5.19.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.19.0) adds `notion.comments.update()` and `notion.comments.delete()`.
### Markdown body parameter for comments
The [Create comment](/reference/create-a-comment) endpoint now accepts an optional `markdown` string body parameter as an alternative to `rich_text`. Exactly one of `rich_text` or `markdown` must be provided. See the [endpoint reference](/reference/create-a-comment#comment-body-format) for supported formatting and usage details.
**SDK support**: `@notionhq/client` [`v5.17.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.17.0) includes typed support for the `markdown` comment body parameter.
### What's new
The [Developer Terms](https://www.notion.so/notion/Developer-Terms-ba4131408d0844e08330da2cbb225c20) have been updated with clarifications to scope, revisions to the Feedback provision, and other minor revisions.
### Heading 4 block type
[`heading_4`](/reference/block#headings) is now a supported block type. You can create, read, and update heading 4 blocks through the [Append block children](/reference/patch-block-children), [Retrieve a block](/reference/retrieve-a-block), and [Update a block](/reference/update-a-block) endpoints, matching the existing `heading_1`, `heading_2`, and `heading_3` block types.
### Tab item icons
[Paragraph blocks](/reference/block#paragraph) that are direct children of [tab blocks](/reference/block#tab) now support an optional [`icon`](/reference/emoji-and-icon) field. You can set icons on tab items when creating tabs via [Append block children](/reference/patch-block-children) or [Create a page](/reference/post-page), and update them via [Update a block](/reference/update-a-block). Icons on paragraphs that are not tab items are rejected with a validation error.
### "me" relative filter for people properties
[People filter conditions](/reference/filter-data-source-entries#people) now accept `"me"` as a value for `contains` and `does_not_contain`, in addition to user UUIDs. For [public integrations](/guides/get-started/overview#connection-types), `"me"` resolves to the user who authorized the connection. For [internal integrations](/guides/get-started/overview#connection-types), `"me"` does not resolve to a user — a `contains: "me"` filter will return no results and a `does_not_contain: "me"` filter will match all entries. Works across [database queries](/reference/post-database-query-filter#people), [data source queries](/reference/filter-data-source-entries#people), [view filters, and quick filters](/guides/data-apis/working-with-views#quick-filters).
### Relative date filter values
Date filter conditions that accept an [ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) string (`equals`, `before`, `after`, `on_or_before`, `on_or_after`) now also accept the following relative date values: `"today"`, `"tomorrow"`, `"yesterday"`, `"one_week_ago"`, `"one_week_from_now"`, `"one_month_ago"`, `"one_month_from_now"`. These are resolved at query time relative to the current date. See the [date filter reference](/reference/filter-data-source-entries#date) for details.
### View API fixes
Several fixes to the [views API](/guides/data-apis/working-with-views):
* **Percent-encoded property IDs**: Property IDs returned by the API (e.g. `%7DUlu`) are now correctly resolved when used in view filters, sorts, group-by, and other property references.
* **`width: 0` rejected**: Column widths must now be at least `1`. A width of `0` was previously accepted but had no effect.
* **Partial `properties` list**: Specifying a subset of properties in a view now correctly hides unlisted properties instead of showing all properties.
**SDK support**: `@notionhq/client` [`v5.16.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.16.0) includes typed support for heading 4, tab item icons, and the `"me"` person filter value.
### Tab block support
[Tab blocks](/reference/block#tab) are now a supported block type in the API. Use tabs to organize content into labeled sections within a page.
* **Read**: [Retrieve a block](/reference/retrieve-a-block) and [Retrieve block children](/reference/get-block-children) return tab blocks with `type: "tab"` and an empty `tab: {}` object. Each tab within the container is a [paragraph](/reference/block#paragraph) block — the `rich_text` is the tab label, the `icon` is the tab icon, and the `children` contain the tab's content.
* **Create**: [Append block children](/reference/patch-block-children) accepts `type: "tab"` blocks. Each tab is a paragraph block with nested children and an optional `icon`. Only paragraph blocks can be direct children of a tab block.
### Writable verification property
The [`verification`](/reference/page-property-values#verification) property on wiki database pages can now be set and updated via the [Create page](/reference/post-page) and [Update page](/reference/patch-page) endpoints. Set `state` to `"verified"` or `"unverified"`, with an optional `date` object for expiration. The `verified_by` field is automatically set to the acting integration and cannot be overridden.
### Native icons and custom emoji listing
Two icon-related improvements:
* **Native Notion icons**: A new `type: "icon"` variant is available on all [`icon`](/reference/emoji-and-icon#icon) fields (pages, databases, callout blocks). Specify an icon by `name` and optional `color` (defaults to `"gray"`). Previously, native icons were returned as `type: "external"` with SVG URLs — they are now returned in the structured `icon` format.
* **Custom emoji listing**: A new [List custom emojis](/reference/list-custom-emojis) endpoint (`GET /v1/custom_emojis`) retrieves workspace custom emojis with cursor pagination and an optional `name` filter for exact-match lookups.
**SDK support**: `@notionhq/client` [`v5.15.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.15.0) adds `notion.customEmojis.list()` and typed support for tab blocks, verification writes, and native icons.
### Views API
We've launched the [`/v1/views` API](/guides/data-apis/working-with-views). Eight new endpoints let integrations programmatically manage database views — the same view presets that users create in the Notion UI:
* [Create](/reference/create-view), [retrieve](/reference/retrieve-a-view), [update](/reference/update-a-view), and [delete](/reference/delete-view) views on any database.
* [List views](/reference/list-views) for a database or across the workspace by data source.
* [Query a view](/reference/create-view-query) to fetch pages using the view's saved filter and sort configuration, with [pagination](/reference/get-view-query-results) support.
[Supported view types](/guides/data-apis/working-with-views#view-configuration) include table, board, calendar, timeline, gallery, list, form, chart, map, and dashboard. Views can be configured with filters, sorts, [quick filters](/guides/data-apis/working-with-views#quick-filters), and type-specific layout settings like grouping, cover images, subtasks, and chart options.
Dashboard views support a full grid layout with [widget placement](/guides/data-apis/working-with-views#widget-placement) — add, position, and remove widget views within rows.
Three new [webhook events](/reference/webhooks/view-created) (`view.created`, `view.updated`, `view.deleted`) are available on API version `2025-09-03` and later.
**SDK support**: `@notionhq/client` [`v5.14.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.14.0) adds `notion.views.*` and `notion.views.queries.*` methods.
### Status property support
You can now [create and update status properties](/reference/property-object#status) through the Notion API and [Notion MCP](/guides/mcp/mcp). Previously, status properties were read-only — they could be queried but not created or modified via the API.
* **Create**: pass `{ status: {} }` in a [Create database](/reference/create-database) or [Create data source](/reference/create-a-data-source) request to add a status property with default options (Not started, In progress, Done). Custom initial options are also supported.
* **Update**: add new options to an existing status property via [Update data source](/reference/update-a-data-source), following the same pattern as select and multi\_select.
* **MCP**: the `notion-create-database` and `notion-update-data-source` tools now support the `STATUS` column type in their schema definitions.
### New API version: `2026-03-11`
We've released **Notion API version `2026-03-11`** with three breaking changes that simplify and modernize the API surface:
* **`after` replaced by `position`**: The [Append block children](/reference/patch-block-children) endpoint now uses a `position` object instead of a flat `after` string parameter, enabling more flexible block placement (including `start` and `end` positioning).
* **`archived` replaced by `in_trash`**: All endpoints now use `in_trash` instead of `archived` in both request parameters and response bodies. The `archived` field was [deprecated in April 2024](/page/changelog#changes-for-april-2024) and is now fully removed in this version.
* **`transcription` renamed to `meeting_notes`**: The `transcription` block type has been renamed to `meeting_notes` across all block endpoints.
Most integrations only need simple find-and-replace updates. See the [upgrade guide](/guides/get-started/upgrade-guide-2026-03-11) for step-by-step instructions.
**SDK support**: `@notionhq/client` [`v5.12.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.12.0) adds support for `2026-03-11`. Upgrade the SDK and set `notionVersion: "2026-03-11"` to opt in.
### Notion MCP: new view tools
Two new tools are available in [Notion MCP](/guides/mcp/mcp):
* **`notion-create-view`** — Create new database views with filters, sorts, grouping, display properties, and layout-specific settings (calendar, timeline, etc.).
* **`notion-update-view`** — Update an existing view's configuration. Accepts `view://` URIs, Notion URLs with `?v=`, or bare UUIDs.
See [Supported tools](/guides/mcp/mcp-supported-tools) for details and example prompts.
### Markdown content API improvements
The [Update page markdown](/reference/update-page-markdown) endpoint now supports two additional command types:
* **`update_content`** — Make targeted edits with an array of search-and-replace operations (`old_str` / `new_str`). Recommended for precise, multi-site edits.
* **`replace_content`** — Replace the entire page content with new markdown in a single operation.
We recommend `update_content` and `replace_content` over the older `insert_content` and `replace_content_range` commands. See [Working with markdown content](/guides/data-apis/working-with-markdown-content) for usage examples.
### Template timezone parameter
The [Create page](/reference/post-page) and [Update page](/reference/patch-page) endpoints now accept an optional `timezone` field inside the `template` parameter. This controls how template variables like `@now` and `@today` resolve — for example, `"America/New_York"` ensures dates reflect Eastern Time instead of defaulting to UTC. See the [Creating pages from templates](/guides/data-apis/creating-pages-from-templates) guide for details.
* The [`GET /v1/pages/:page_id/markdown`](/reference/retrieve-page-markdown) endpoint is now available to **internal integrations** (workspace-level bots), in addition to public integrations.
* Released [`v5.11.1`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.11.1) of our TS/JS SDK. `UnsupportedBlockObjectResponse` now includes a `block_type` string field indicating the underlying block type.
We released [`v5.10.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.10.0) and [`v5.11.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.11.0) of our SDK for JavaScript and TypeScript. Here's what's new in the Notion API:
### Markdown content API
Three new endpoints let you create, read, and update page content using [enhanced markdown](/guides/data-apis/enhanced-markdown) instead of the block-based API:
* [`POST /v1/pages`](/reference/post-page) now accepts a `markdown` parameter as an alternative to `children`.
* [`GET /v1/pages/:page_id/markdown`](/reference/retrieve-page-markdown) retrieves a page's full content as enhanced markdown.
* [`PATCH /v1/pages/:page_id/markdown`](/reference/update-page-markdown) inserts or replaces content using enhanced markdown with ellipsis-based selections.
See [Working with markdown content](/guides/data-apis/working-with-markdown-content) and the [Enhanced markdown format reference](/guides/data-apis/enhanced-markdown) for details.
### AI meeting notes
* The `GET /v1/pages/:page_id/markdown` endpoint supports an `include_transcript` query parameter to include full meeting note transcripts in the response.
* Added support for the [`transcription` block type](/reference/block#transcription), enabling integrations to read AI meeting notes metadata — including title, status, calendar event details, and pointers to summary, notes, and transcript content blocks.
### SDK improvements
* **Automatic retry with exponential backoff** — the SDK now retries failed requests automatically with configurable backoff ([v5.10.0](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.10.0)).
* **Markdown endpoint methods** — `pages.retrieveMarkdown()` and `pages.updateMarkdown()` ([v5.11.0](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.11.0)).
### Notion MCP improvements
Highlighting recent changes to [Notion MCP](https://developers.notion.com/docs/mcp):
* Create and fetch **comments on blocks**, not just pages.
* View **Notion Sites** pages via the fetch tool.
* Fetch AI **meeting transcripts** and query meeting notes efficiently with the new `notion-query-meeting-notes` tool.
* Fetch an **individual data source** by ID or URL within a database.
* **\~91% context token reduction** in `notion-create-database` and `notion-update-data-source` tools by switching to SQL DDL-based schemas.
* Added `update_verification` command to the `notion-update-page` tool.
* Flattened `notion-update-page` tool parameters and fixed schema issues for improved compatibility with MCP clients.
* **Enterprise governance**: audit logging for MCP tool usage and admin tool allowlisting.
We recommend reconnecting Notion MCP in your third-party AI tools to ensure you have the most up-to-date tools and resources.
We [released `v5.7.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.7.0) of our SDK for JavaScript and TypeScript. Since the last changelog entry, we've added the following fixes and improvements to the Notion API:
* Introduce [Move page](/reference/move-page) API to change the `parent` of an existing page.
* TS/JS example projects extracted to a new open-source project: [`notion-cookbook`](https://github.com/makenotion/notion-cookbook/tree/main/examples).
* Add support for [customizing the `position` of a new page](https://developers.notion.com/reference/post-page#choosing-a-parent) within the parent page.
* New APIs to power the flow described in our [Creating pages from templates](/guides/data-apis/creating-pages-from-templates) guide:
* Introduce [List data source templates](/reference/list-data-source-templates) endpoint.
* Introduce [`template` parameter](https://developers.notion.com/reference/post-page#setting-up-page-content) to Create Page API.
* Introduce [`template`](https://developers.notion.com/reference/patch-page#applying-a-page-template) and [`erase_content` parameters](https://developers.notion.com/reference/patch-page#erasing-content-from-a-page) to Update Page API.
Highlighting recent LLM-facing changes to [Notion MCP](https://developers.notion.com/docs/mcp), our remote Model Context Protocol (MCP) server for AI tools:
* Released `notion-query-data-sources` tool to Enterprise Notion workspaces with access to Notion AI.
* Tool consolidation: `notion-get-user` has been removed & its functionality has been rolled into `notion-get-users`.
* Fixed a bug causing child content to be deleted by the `notion-update-page` tool when using `replace_content` and `replace_content_range` modes.
* Removed Notion-flavored Markdown specification from `notion-create-pages` tool to conserve context tokens, since it exists behind a dedicated MCP Resource as well.
We recommend reconnecting Notion MCP in your third-party AI tools to ensure you have the most up-to-date tools and resources.
We [released `v5.1.0`](https://github.com/makenotion/notion-sdk-js/releases/tag/v5.1.0) of `@notionhq/client`, our SDK for JavaScript and TypeScript. This includes the following fixes and improvements:
* Add support for `is_locked` boolean parameter on update page and database APIs (to update whether a page is locked in the Notion app UI)
* `dataSource.update`: add support for changing a data source's `parent` database
* Remove `page_id` as a possible `parent` for `CreateDataSourceBodyParameters`
* Add `request_id` to Client log lines
As noted in the [library's README](https://github.com/makenotion/notion-sdk-js?tab=readme-ov-file#requirements-and-compatibility), v5 and above of the SDK isn't compatible with API versions older than `2025-09-03`. See the [upgrade guide](/guides/get-started/upgrade-guide-2025-09-03) to learn more.
### Important API update coming September 3rd
We're introducing multi-source databases to Notion! Our new API version `2025-09-03` separates "**databases**" (containers) from "**data sources**" (tables), unlocking powerful new organizational capabilities.
### What you need to know:
* Current integrations continue working with single-source databases
* Update to the new API version to support multi-source databases
* We're introducing the concept of API versioning to [integration webhooks](/reference/webhooks) as well
Start upgrading your integrations now to ensure a smooth transition when users begin creating additional data sources starting from September 3rd.
**Full details and migration guide**: [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03)
**General information about API versioning**: [Versioning](/reference/versioning)
### What's new
* Revised **Section 1.1** to refine the scope of application of the [Developer Terms](https://www.notion.so/Developer-Terms-ba4131408d0844e08330da2cbb225c20).
* Revised **Section 3.1** to clarify prohibited uses of the API and created a new **Section 3.2** for formatting purposes
### What's new
We are excited to announce an update to our Notion Public API token format.
Starting September 25, 2024, newly generated Public API tokens will automatically use the **`ntn_`** prefix instead of the\*\*`secret_`\*\* prefix.
### Why the change?
This change is part of our ongoing efforts to improve the security of our API. By introducing the **`ntn_`** prefix, we aim to:
* Enhance compatibility with secret scanners and other security tools, making it easier to identify and manage Notion API tokens.
* Provide a clearer distinction between Notion API tokens and other types of secrets, reducing the risk of misconfiguration and improving overall security.
### What do you need to do?
* New Integrations: For any new integrations, the tokens will be automatically generated with the **`ntn_`** prefix. Simply generate your tokens as usual through the Notion API settings page.
* Existing Tokens: All existing tokens with the secret\_ prefix will continue to work without any changes. There is no immediate need to update your existing integrations.
* Token Format: We strongly advise against using regular expressions (regex) to identify or validate Notion Public API tokens. The token format may change over time, and relying on regex patterns could lead to false positives or negatives. Instead, treat the token as an opaque string and use it as provided.
* Best Practices: To handle Notion API tokens securely:
* Store tokens securely using appropriate encryption methods.
* Use Notion's official SDKs or libraries when available, as they handle token management correctly.
* Validate tokens by making authenticated requests to Notion's API rather than parsing the token itself.
### Questions or concerns?
If you have any questions or need assistance with this transition, please feel free to reach out to our support team or visit our docs.
### What's new
Revised **Section 3.1** of the [Developer Terms](https://www.notion.so/Developer-Terms-ba4131408d0844e08330da2cbb225c20) to include additional security and data use restrictions.
### What's new
* Added: New property `in_trash` to indicate whether a page/block/database has been deleted or placed in "Trash".
* `in_trash` is the preferred field going forward. The `archived` property is a deprecated alias for `in_trash` and may be removed in a future API version. New integrations should use `in_trash` exclusively.
### What's new
* We added support for reading and writing names to `file` blocks in the public API. Read more here.
* We fixed the types in the SDK to support appending `table` and `column` blocks as children of `toggle` blocks.
* We updated the emoji and timezones available in the SDK.
* We added support for `australian_dollar` in the `format` field of number database properties.
### What's new
* The [Examples](/page/examples) page was updated with all our most recent demo code. We've organized these sample integrations by level of experience with the Public API to help developers who are newer to the Public API find introductory code more easily.
* A note was added to all API endpoint documentation directing developers to review the [Status codes](/reference/status-codes#error-codes) page for a complete list of error codes that can be returned by API requests.
* A clarification was added to the [Request limits](/reference/request-limits) page and [Append block children endpoint](/reference/patch-block-children) documentation to indicate the current limit for appending a list of block children per API request. Up to 100 block children can be appended at a time.
### What's new
* The [updates](/page/changelog) related to the [Formulas 2.0 launch](https://twitter.com/NotionHQ/status/1699828805408550971?s=20) are now live in the Public API. These changes will not impact most developers using the Public API; however, please note that the formatting of [`formula.expression`](/reference/property-object#formula), which is returned when [retrieving a database](/reference/retrieve-a-database) with a [Formula property](/reference/property-object#formula), has changed. See Notion's Help Center articles for more information on the Formula 2.0 changes:
* [Formulas 2.0: How to use Notion's new and improved formulas with your existing setups](https://www.notion.so/help/guides/new-formulas-whats-changed)
* [How to write Notion formulas that extend the capabilities of your databases](https://www.notion.so/help/guides/write-formulas-that-extend-capabilities-of-databases)
* The example for the [Formula database property](/reference/property-object#formula) was updated to align with the new Formula 2.0 launch.
* [New sample code](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/intro-to-notion-api) was added to the [Notion SDK for JavaScript's `examples`](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript) directory. This new example demonstrates how to use the Public API with basic and intermediate levels of difficulty.
**Looking for older updates?**
Changelog entries from before September 2023 are now kept in a separate page: [Historical changelog](/guides/resources/historical-changelog).
# Examples
Source: https://developers.notion.com/page/examples
## Introductory
In this introductory codebase, start by learning the basics of Notion's Public API: creating a new block.
Build on the previous example by creating a block in Notion and adding a link to it.
Extend the previous example further by styling a block of text that links to an external website.
This connection shows how to get a list of blocks from a Notion page and parse the text from any type of block.
## Intermediate
Create your first Notion database with a defined set of properties.
Build on the previous example by creating a database and adding new pages to it.
Learn how to filter your database rows (pages) after creating them from scratch.
Filter and sorts pages after adding them to a new database.
Create, send, and attach a file upload to a page's contents and as a comment attachment.
Learn how to build a Notion connection with an interactive front-end using Express.js and the Notion SDK for JavaScript.
## Advanced
This connection populates a Notion database with track metadata from a Spotify playlist.
This connection populates a Notion database with Mailchimp campaign information, including subscriber contact information.
This connection syncs a Strava athlete's activity metadata within a Notion database.
This connection finds the first database that your bot has access to, and creates correctly-typed random rows of data.
This Notion connection syncs GitHub Issues for a specific repo to a Notion database. This example shows a one-way sync — changes in GitHub cause an update in Notion.
This Notion connection sends an email whenever the *Status* property of a page in a database is updated. This sample shows how to use Notion to cause an external action. In this case, the connection sends emails using SendGrid's API.
This Notion connection updates Notion tasks when a linked Github PR is closed/merged. This connection requires the Notion task link be mentioned in the PR description.
# Add users to a legal hold
Source: https://developers.notion.com/reference/admin/add-legal-hold-users
openapi-adminApi.json POST /v1/legal_holds/{legal_hold_id}/users
Add users to a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:write`
# Create a legal hold
Source: https://developers.notion.com/reference/admin/create-legal-hold
openapi-adminApi.json POST /v1/legal_holds
Create a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:write`
# Enqueue a workspace export
Source: https://developers.notion.com/reference/admin/enqueue-space-export
openapi-adminApi.json POST /v1/spaces/{space_id}/exports
Enqueue a workspace export.
The organization bot token must have the following scopes:
* `workspace:export`
# Export a legal hold
Source: https://developers.notion.com/reference/admin/export-legal-hold
openapi-adminApi.json POST /v1/legal_holds/{legal_hold_id}/export
Export a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:export`
# Retrieve a legal hold
Source: https://developers.notion.com/reference/admin/get-legal-hold
openapi-adminApi.json GET /v1/legal_holds/{legal_hold_id}
Retrieve a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:read`
# Get workspace export status
Source: https://developers.notion.com/reference/admin/get-space-export-status
openapi-adminApi.json GET /v1/spaces/{space_id}/exports/{export_job_id}
Get workspace export status.
The organization bot token must have the following scopes:
* `workspace:export`
# Introduction
Source: https://developers.notion.com/reference/admin/intro
Learn the conventions, authentication, and pagination patterns used for Notion's Admin API.
The Admin API can be used to manage Notion resources associated with your organization. Use the navigation on the left to find details for endpoints used in the API.
The Admin API is only available to organizations and workspaces that are actively on the [enterprise plan](https://www.notion.com/enterprise).
## Creating & managing tokens
Every request is authenticated with an organization bot token tied to your Notion organization. Organization owners can manage these tokens in the [organization console](https://www.notion.com/help/organization-level-controls), where they can create, edit, or revoke any active tokens.
## Conventions
The base URL to send all API requests is `https://api.notion.com/admin`. HTTPS is required for all API requests.
The Notion API follows RESTful conventions when possible, with most operations performed via `GET`, `POST`, `PATCH`, and `DELETE` requests on page and database resources. Request and response bodies are encoded as JSON.
## Code samples
Samples requests and responses are shown for each endpoint. Requests are shown using [cURL](https://curl.se/). These samples make it easy to copy, paste, and modify as you build your connection.
## Pagination
Endpoints that return lists of objects support cursor-based pagination requests. By default, Notion returns ten items per API call. If the number of items in a response from a support endpoint exceeds the default, then you can use pagination to request a specific set of the results and/or to limit the number of returned items.
# List pages on a legal hold
Source: https://developers.notion.com/reference/admin/list-legal-hold-pages
openapi-adminApi.json GET /v1/legal_holds/{legal_hold_id}/spaces/{space_id}/pages
List pages on a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:read`
# List users on a legal hold
Source: https://developers.notion.com/reference/admin/list-legal-hold-users
openapi-adminApi.json GET /v1/legal_holds/{legal_hold_id}/users
List users on a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:read`
# List workspaces on a legal hold
Source: https://developers.notion.com/reference/admin/list-legal-hold-workspaces
openapi-adminApi.json GET /v1/legal_holds/{legal_hold_id}/workspaces
List workspaces on a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:read`
# List legal holds
Source: https://developers.notion.com/reference/admin/list-legal-holds
openapi-adminApi.json GET /v1/legal_holds
List all legal holds belonging to your organization.
The organization bot token must have the following scopes:
* `legal-hold:read`
Returns all legal holds for your organization, both active and released.
The API does not guarantee a particular sort order for returned legal holds.
# List MCP client connections in an organization
Source: https://developers.notion.com/reference/admin/list-mcp-client-connections
openapi-adminApi.json GET /v1/mcp_client_connections
List members' connections to Notion through MCP clients.
The organization bot token must have the following scopes:
* `mcp-client-connection:read`
This endpoint lists OAuth connections that members use to connect MCP clients, such as Claude Code or Codex, to Notion MCP. It does not include MCP requests authenticated with [personal access tokens](/guides/get-started/personal-access-tokens) or workspace-owned internal connections.
For workspace settings that control which MCP clients members can connect, see [Admin controls for Notion MCP](https://www.notion.com/help/notion-mcp#admin-controls-for-mcp).
## Results
Each result identifies a member, an MCP client, and the workspaces where they are connected. Within a result, `created_at` is the earliest connection time and `last_active_at` is the most recent activity time.
Connections for the same member and client can appear in one result across several workspaces. If your organization uses enterprise-managed connections, the API returns separate results when `is_enterprise_managed` or `idp_issuer` differs. `idp_issuer` is present only for enterprise-managed connections.
## Filter results
Use bracket encoding to filter by one or more members or workspaces:
```text theme={null}
?user_ids[]=aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa&workspace_ids[]=bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
```
Filters only match workspaces in the organization that owns the token. Unknown IDs and IDs outside the organization return no results. A `workspace_ids` filter also limits the `workspaces` array in each result.
## Identify an MCP client
For recognized MCP clients, Notion provides `client.name` and `client.type`. A custom or unregistered client can provide its own name, and its type is `other`. Use `client.key`, not the name, to identify the client in later API requests.
# List personal access tokens in a workspace
Source: https://developers.notion.com/reference/admin/list-personal-access-tokens
openapi-adminApi.json GET /v1/spaces/{space_id}/personal_access_tokens
List active, expired, and revoked personal access tokens in a workspace.
The organization bot token must have the following scopes:
* `personal-access-token:read`
This endpoint returns personal access token (PAT) records, but never returns token values. Use the `id` from a result as the `bot_id` when you [revoke the token](/reference/admin/revoke-personal-access-token).
## Filter results
Use `status` to return active, expired, or revoked tokens. Use `creator_ids` to filter by the members who created the tokens, and use `search` to match a token name.
Use bracket encoding to filter by more than one status or creator:
```text theme={null}
?status[]=active&status[]=expired&creator_ids[]=aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
```
To learn how PATs work and how members create them, see [Personal access tokens](/guides/get-started/personal-access-tokens).
# Release a legal hold
Source: https://developers.notion.com/reference/admin/release-legal-hold
openapi-adminApi.json POST /v1/legal_holds/{legal_hold_id}/release
Release a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:write-high-impact`
# Remove a user from a legal hold
Source: https://developers.notion.com/reference/admin/remove-legal-hold-user
openapi-adminApi.json DELETE /v1/legal_holds/{legal_hold_id}/users/{user_id}
Remove a user from a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:write`
# Revoke an MCP client connection
Source: https://developers.notion.com/reference/admin/revoke-mcp-client-connection
openapi-adminApi.json POST /v1/mcp_client_connections/revoke
Revoke a member's active connection to Notion from an MCP client.
The organization bot token must have the following scopes:
* `mcp-client-connection:write-high-impact`
This endpoint revokes the active OAuth tokens for one member and MCP client in a workspace. Use [List MCP client connections](/reference/admin/list-mcp-client-connections) to get the `client_key`.
The response gives the number of tokens revoked. The endpoint returns the same not-found response when the member, workspace, or connection does not exist or is outside the organization.
Revoking tokens disconnects the member, but does not prevent them from connecting the client again if workspace policy allows it. To control which MCP clients members can connect, see [Admin controls for Notion MCP](https://www.notion.com/help/notion-mcp#admin-controls-for-mcp).
## Enterprise-managed access
If your organization uses enterprise-managed connections, you can [deny a member's enterprise-managed access](/reference/admin/update-mcp-client-connection-enterprise-managed-access) to revoke their current tokens and prevent them from connecting again through enterprise-managed authentication. This setting applies to the member and workspace, not only to the client identified by `client_key`.
# Revoke a personal access token in a workspace
Source: https://developers.notion.com/reference/admin/revoke-personal-access-token
openapi-adminApi.json DELETE /v1/spaces/{space_id}/personal_access_tokens/{bot_id}
Permanently revoke a personal access token in a workspace.
The organization bot token must have the following scopes:
* `personal-access-token:write-high-impact`
Use [List personal access tokens](/reference/admin/list-personal-access-tokens) to get the token's `id`, then pass it as `bot_id`.
After revocation, the token can no longer authenticate API requests or access Notion Workers. Repeating the request leaves the token revoked and returns an empty response.
The workspace must belong to the organization that owns the organization bot token. The endpoint returns the same not-found response when the workspace or PAT does not exist, is outside the organization, or does not match the request.
# Revoke a managed user's sessions
Source: https://developers.notion.com/reference/admin/revoke-user-session
openapi-adminApi.json POST /v1/managed_users/revoke_session
Revoke a managed user's sessions.
The organization bot token must have the following scopes:
* `managed-user-session:write`
# Scopes
Source: https://developers.notion.com/reference/admin/scopes
Admin API scopes control an organization bot token's access.
When you create an organization bot token, you assign scopes that control which resources the token can access and which actions it can take.
Each scope combines a resource and a capability, such as `legal-hold:read`.
## Scope resources
| Scope resource | Controls access to |
| :---------------------- | :------------------------------------------------------ |
| `legal-hold` | Legal hold data and members |
| `managed-user-session` | Managed users' active sessions |
| `mcp-client-connection` | Members' MCP client connections |
| `personal-access-token` | Personal access tokens in the organization's workspaces |
| `workspace` | Workspace data and settings |
This table may not include every scope. Each endpoint's reference lists the exact scope it requires.
## Scope capabilities
| Scope capability | Allows the token to |
| :------------------ | :------------------------------------------------------------- |
| `read` | View a resource |
| `write` | Modify a resource |
| `write-high-impact` | Make sensitive changes, such as revoking credentials or access |
| `export` | Export a resource |
# Update a legal hold
Source: https://developers.notion.com/reference/admin/update-legal-hold
openapi-adminApi.json PATCH /v1/legal_holds/{legal_hold_id}
Update a legal hold.
The organization bot token must have the following scopes:
* `legal-hold:write`
# Deny or restore enterprise-managed MCP client access
Source: https://developers.notion.com/reference/admin/update-mcp-client-connection-enterprise-managed-access
openapi-adminApi.json PUT /v1/mcp_client_connections/enterprise_managed_access
Deny or restore a member's access through enterprise-managed MCP connections.
The organization bot token must have the following scopes:
* `mcp-client-connection:write-high-impact`
Use this endpoint only for enterprise-managed connections. It controls one member's enterprise-managed MCP access in one workspace. It does not apply to MCP client connections that members authorize themselves.
Set `access` to `denied` to block future connections and revoke the member's current tokens. Set it to `allowed` to remove the block.
You can repeat the same request safely. Repeating a deny keeps access blocked and revokes any new tokens. Repeating a restore keeps access allowed. You can restore access only when enterprise-managed connections are available for the workspace.
The workspace and member must belong to the organization that owns the token. The endpoint returns the same not-found response for invalid targets and targets outside the organization.
# Versioning
Source: https://developers.notion.com/reference/admin/versioning
Learn about API change management and how to set the appropriate version in your connection.
The Admin API is versioned. Our API versions are named for the date the version is released. For example, our latest version is .
Set the version by including a `Notion-Version` header. Setting this header is **required**.
```bash cURL theme={null}
curl https://api.notion.com/admin/v1/legal_holds \
-H "Authorization: Bearer adm_t1CdN9S8yicG5eWLUOfhcWaOscVnFXnsJM9cWeCdTNfhbt"
-H "Notion-Version: 2026-06-01"
```
**Required Header**
The `Notion-Version` header must be included in all REST API requests. This ensures the Notion API response is consistent with what your code expects.
The most recent `Notion-Version` is .
# Authentication
Source: https://developers.notion.com/reference/authentication
Learn how to authenticate your connection requests using bearer tokens.
Requests use the HTTP `Authorization` header to both authenticate and authorize operations. The Notion API accepts bearer tokens in this header. Bearer tokens are provided when you create an [internal connection](/guides/get-started/internal-connections), create a [personal access token](/guides/get-started/personal-access-tokens), or complete the OAuth flow for a [public connection](/guides/get-started/public-connections).
```curl cURL theme={null}
curl 'https://api.notion.com/v1/users' \
-H 'Authorization: Bearer '"$NOTION_ACCESS_TOKEN"'' \
-H "Notion-Version: 2026-03-11"
```
Inside Notion, users will see updates made by connections attributed according to the token type. Internal connections use their bot identity, public connections act for the user who authorized them, and PATs act as the user who created the token.
Using the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js), a bearer token can be passed once to initialize a `Client` and the client can be used to send multiple authenticated requests.
```javascript Notion SDK for JS theme={null}
const { Client } = require('@notionhq/client');
const client = new Client({ auth: process.env.NOTION_ACCESS_TOKEN });
```
Learn more in the [Authorization guide](/guides/get-started/authorization).
# Block
Source: https://developers.notion.com/reference/block
A block object represents a piece of content within Notion. The API translates the headings, toggles, paragraphs, lists, media, and more that you can interact with in the Notion UI as different [block type objects](/reference/block#block-type-objects).
For example, the following block object represents a `Heading 2` in the Notion UI:
```json Example Block Object expandable theme={null}
{
"object": "block",
"id": "c02fc1d3-db8b-45c5-a222-27595b15aea7",
"parent": {
"type": "page_id",
"page_id": "59833787-2cf9-4fdf-8782-e53db20768a5"
},
"created_time": "2022-03-01T19:05:00.000Z",
"last_edited_time": "2022-07-06T19:41:00.000Z",
"created_by": {
"object": "user",
"id": "ee5f0f84-409a-440f-983a-a5315961c6e4"
},
"last_edited_by": {
"object": "user",
"id": "ee5f0f84-409a-440f-983a-a5315961c6e4"
},
"has_children": false,
"in_trash": false,
"type": "heading_2",
"heading_2": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "green"
},
"plain_text": "Lacinato kale",
"href": null
}
],
"color": "default",
"is_toggleable": false
}
}
```
Use the [Retrieve block children](/reference/get-block-children) endpoint to list all of the blocks on a page.
## Keys
Fields marked with an \* are available to connections with any capabilities. Other properties require read content capabilities in order to be returned from the Notion API. Consult the [connection capabilities reference](/reference/capabilities) for details.
| Field | Type | Description | Example value |
| :----------------- | :---------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------- |
| `object`\* | `string` | Always `"block"`. | `"block"` |
| `id`\* | `string` (UUIDv4) | Identifier for the block. | `"7af38973-3787-41b3-bd75-0ed3a1edfac9"` |
| `parent` | `object` | Information about the block's parent. See [Parent object](/reference/parent-object). | `{ "type": "block_id", "block_id": "7d50a184-5bbe-4d90-8f29-6bec57ed817b" }` |
| `type` | `string` (enum) | Type of block. Possible values are:
- [`"bookmark"`](/reference/block#bookmark) - [`"breadcrumb"`](/reference/block#breadcrumb) - [`"bulleted_list_item"`](/reference/block#bulleted-list-item) - [`"callout"`](/reference/block#callout) - [`"child_database"`](/reference/block#child-database) - [`"child_page"`](/reference/block#child-page) - [`"column"`](/reference/block#column-list-and-column) - [`"column_list"`](/reference/block#column-list-and-column) - [`"divider"`](/reference/block#divider) - [`"embed"`](/reference/block#embed) - [`"equation"`](/reference/block#equation) - [`"file"`](/reference/block#file) - [`"heading_1"`](/reference/block#headings) - [`"heading_2"`](/reference/block#headings) - [`"heading_3"`](/reference/block#headings) - [`"heading_4"`](/reference/block#headings) - [`"image"`](/reference/block#image) - [`"link_preview"`](/reference/block#link-preview) - [`"numbered_list_item"`](/reference/block#numbered-list-item) - [`"paragraph"`](/reference/block#paragraph) - [`"pdf"`](/reference/block#pdf) - [`"quote"`](/reference/block#quote) - [`"synced_block"`](/reference/block#synced-block) - [`"table"`](/reference/block#table) - [`"table_of_contents"`](/reference/block#table-of-contents) - [`"table_row"`](/reference/block#table-rows) - [`"template"`](/reference/block#template) - [`"to_do"`](/reference/block#to-do) - [`"toggle"`](/reference/block#toggle-blocks) - [`"transcription"`](/reference/block#transcription) - [`"unsupported"`](/reference/block#unsupported) - [`"video"`](/reference/block#video) | `"paragraph"` |
| `created_time` | `string` ([ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this block was created. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T19:10:04.968Z"` |
| `created_by` | [Partial User](/reference/user) | User who created the block. | `{"object": "user","id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `last_edited_time` | `string` ([ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this block was last updated. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T19:10:04.968Z"` |
| `last_edited_by` | [Partial User](/reference/user) | User who last edited the block. | `{"object": "user","id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `archived` | `boolean` | **Deprecated.** Use `in_trash` instead. This is an alias for `in_trash` and always returns the same value. | `false` |
| `in_trash` | `boolean` | Whether the block has been trashed. Use this field to check if a block is in the trash, and as a body parameter in [Update a block](/reference/update-a-block) to trash or restore a block. | `false` |
| `has_children` | `boolean` | Whether or not the block has children blocks nested within it. | `true` |
| `{type}` | [`block type object`](/reference/block#block-type-objects) | An object containing type-specific block information. | Refer to the [block type object section](/reference/block#block-type-objects) for examples of each block type. |
#### Block types that support child blocks
Some block types contain nested blocks. The following block types support child blocks:
* [Bulleted list item](/reference/block#bulleted-list-item)
* [Callout](/reference/block#callout)
* [Child database](/reference/block#child-database)
* [Child page](/reference/block#child-page)
* [Column](/reference/block#column-list-and-column)
* [Heading 1](/reference/block#headings), when the `is_toggleable` property is `true`
* [Heading 2](/reference/block#headings), when the `is_toggleable` property is `true`
* [Heading 3](/reference/block#headings), when the `is_toggleable` property is `true`
* [Heading 4](/reference/block#headings), when the `is_toggleable` property is `true`
* [Meeting notes](/reference/block#meeting-notes) (renamed from [Transcription](/reference/block#transcription) in `2026-03-11`)
* [Numbered list item](/reference/block#numbered-list-item)
* [Paragraph](/reference/block#paragraph)
* [Quote](/reference/block#quote)
* [Synced block](/reference/block#synced-block)
* [Table](/reference/block#table)
* [Template](/reference/block#template)
* [To do](/reference/block#to-do)
* [Toggle](/reference/block#toggle-blocks)
**The API does not support all block types.**
Only the block type objects listed in the reference below are supported. Any unsupported block types appear in the structure, but contain a `type` set to `"unsupported"`. The [`unsupported`](/reference/block#unsupported) object includes a `block_type` field that identifies the underlying block type (e.g., `"form"`, `"button"`).
## Block type objects
Every block object has a key corresponding to the value of `type`. Under the key is an object with type-specific block information.
Many block types support rich text. In cases where it is supported, a [`rich_text` object](/reference/rich-text) will be included in the block `type` object. All `rich_text` objects will include a `plain_text` property, which provides a convenient way for developers to access unformatted text from the Notion block.
### Audio
Audio block objects contain a [file object](/reference/file-object) detailing information about the audio file.
```json Example Audio block object theme={null}
{
"type": "audio",
//...other keys excluded
"audio": {
"type": "external",
"external": {
"url": "https://companywebsite.com/files/sample.mp3"
}
}
}
```
#### Supported audio types
The following file types can be attached with external URLs in the API as well as in the Notion app UI:
* `.mp3`
* `.wav`
* `.ogg`
* `.oga`
* `.m4a`
A wider set of audio files is [supported in the File Upload API](/guides/data-apis/working-with-files-and-media) and can be attached using a `file_upload` ID.
#### Supported file upload types
See the [file upload reference](/reference/file-upload) for a list of supported file extensions and content types when attaching a File Upload to a block.
Audio blocks only support file types in the "audio" section of the table.
### Bookmark
Bookmark block objects contain the following information within the `bookmark` property:
| Field | Type | Description |
| :-------- | :------------------------------------------------------ | :---------------------------- |
| `caption` | array of [rich text objects](/reference/rich-text) text | The caption for the bookmark. |
| `url` | string | The link for the bookmark. |
```json Example Bookmark block object theme={null}
{
//...other keys excluded
"type": "bookmark",
//...other keys excluded
"bookmark": {
"caption": [],
"url": "https://companywebsite.com"
}
}
```
### Breadcrumb
Breadcrumb block objects do not contain any information within the `breadcrumb` property.
```json Example Breadcrumb block object theme={null}
{
//...other keys excluded
"type": "breadcrumb",
//...other keys excluded
"breadcrumb": {}
}
```
### Bulleted list item
Bulleted list item block objects contain the following information within the `bulleted_list_item` property:
| Field | Type | Description |
| :---------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text in the `bulleted_list_item` block. |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks (if any) of the `bulleted_list_item` block. |
```json Example Bulleted list item block object theme={null}
{
//...other keys excluded
"type": "bulleted_list_item",
//...other keys excluded
"bulleted_list_item": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
// ..other keys excluded
}],
"color": "default",
"children":[{
"type": "paragraph"
// ..other keys excluded
}]
}
}
```
### Callout
Callout block objects contain the following information within the `callout` property:
| Field | Type | Description |
| :---------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text in the `callout` block. |
| `icon` | `object` | The callout's icon. Can be an [emoji](/reference/emoji-and-icon#emoji), [custom emoji](/reference/emoji-and-icon#custom-emoji), [native icon](/reference/emoji-and-icon#icon), or [file](/reference/file-object). |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
```json Example Callout block object expandable theme={null}
{
//...other keys excluded
"type": "callout",
// ..other keys excluded
"callout": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
// ..other keys excluded
}],
"icon": {
"emoji": "⭐"
},
"color": "default"
}
}
```
### Child database
Child database block objects contain the following information within the `child_database` property:
| Field | Type | Description |
| :------ | :------- | :------------------------------------ |
| `title` | `string` | The plain text title of the database. |
```json Example Child database block theme={null}
{
//...other keys excluded
"type": "child_database",
//...other keys excluded
"child_database": {
"title": "My database"
}
}
```
**Creating and updating `child_database` blocks**
To create or update `child_database` type blocks, use the [Create a database](/reference/create-a-database) and the [Update a database](/reference/update-a-database) endpoints, specifying the ID of the parent page in the `parent` body param.
### Child page
Child page block objects contain the following information within the `child_page` property:
| Field | Type | Description |
| :------ | :------- | :---------------------------------- |
| `title` | `string` | The plain text `title` of the page. |
```json Example Child page block object theme={null}
{
//...other keys excluded
"type": "child_page",
//...other keys excluded
"child_page": {
"title": "Lacinato kale"
}
}
```
**Creating and updating `child_page` blocks**
To create or update `child_page` type blocks, use the [Create a page](/reference/post-page) and the [Update page](/reference/patch-page) endpoints, specifying the ID of the parent page in the `parent` body param.
### Code
Code block objects contain the following information within the `code` property:
| Field | Type | Description |
| :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------- |
| `caption` | `array` of [Rich text object](/reference/rich-text) text objects | The rich text in the caption of the code block. |
| `rich_text` | `array` of [Rich text object](/reference/rich-text) text objects | The rich text in the code block. |
| `language` | - `"abap"` - `"arduino"` - `"bash"` - `"basic"` - `"c"` - `"clojure"` - `"coffeescript"` - `"c++"` - `"c#"` - `"css"` - `"dart"` - `"diff"` - `"docker"` - `"elixir"` - `"elm"` - `"erlang"` - `"flow"` - `"fortran"` - `"f#"` - `"gherkin"` - `"glsl"` - `"go"` - `"graphql"` - `"groovy"` - `"haskell"` - `"html"` - `"java"` - `"javascript"` - `"json"` - `"julia"` - `"kotlin"` - `"latex"` - `"less"` - `"lisp"` - `"livescript"` - `"lua"` - `"makefile"` - `"markdown"` - `"markup"` - `"matlab"` - `"mermaid"` - `"nix"` - `"objective-c"` - `"ocaml"` - `"pascal"` - `"perl"` - `"php"` - `"plain text"` - `"powershell"` - `"prolog"` - `"protobuf"` - `"python"` - `"r"` - `"reason"` - `"ruby"` - `"rust"` - `"sass"` - `"scala"` - `"scheme"` - `"scss"` - `"shell"` - `"sql"` - `"swift"` - `"typescript"` - `"vb.net"` - `"verilog"` - `"vhdl"` - `"visual basic"` - `"webassembly"` - `"xml"` - `"yaml"` - `"java/c/c++/c#"` | The language of the code contained in the code block. |
```json Example Code block object theme={null}
{
// ... other keys excluded
"type": "code",
// ... other keys excluded
"code": {
"caption": [],
"rich_text": [{
"type": "text",
"text": {
"content": "const a = 3"
}
}],
"language": "javascript"
}
}
```
### Column list and column
Column lists are parent blocks for columns. They do not contain any information within the `column_list` property.
```json Example Column list block object theme={null}
{
// ... other keys excluded
"type": "column_list",
// ... other keys excluded
"column_list": {}
}
```
Columns are parent blocks for any block types listed in this reference except for other `column`s. They do not require any information within the `column` property, but a `width_ratio` number between 0 and 1 can be provided to customize the width of a column relative to others in the same column list. When omitted, the default is to use equal widths for all columns. When provided, `width_ratio`s should add up to 1.
Columns can only be appended to `column_list`s.
```json Example Column object theme={null}
{
// ... other keys excluded
"type": "column",
// ... other keys excluded
"column": {
"width_ratio": 0.25
}
}
```
When creating a `column_list` block using [Append block children](/reference/patch-block-children), the `column_list` must have at least two `column`s, and each `column` must have at least one child.
#### Retrieve the content in a column list
Follow these steps to fetch the content in a `column_list`:
Get the `column_list` ID from a query to [Retrieve block children](/reference/get-block-children) for the parent page.
Get the `column` children from a query to Retrieve block children for the `column_list`.
Get the content in each individual `column` from a query to Retrieve block children for the unique `column` ID.
### Divider
Divider block objects do not contain any information within the `divider` property.
```json Example Divider block object theme={null}
{
//...other keys excluded
"type": "divider",
//...other keys excluded
"divider": {}
}
```
### Embed
Embed block objects include information about another website, or an uploaded file, displayed within the Notion UI. The `embed` property contains the following information:
| Field | Type | Description |
| :------------ | :------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url` | `string` | The link that the embed block displays. When the embed is backed by an uploaded file, this is a temporary, signed link; fetch the block again from the API to get a fresh `url` rather than caching it. |
| `file_upload` | [File upload object](/reference/file-upload) | Create and update parameter only. An object with the `id` of a [FileUpload](/reference/file-upload) to embed (for example, a PDF). Attaching an uploaded `.html` file creates an [HTML block](/reference/block#html-blocks). Provide either `url` or `file_upload`, not both. An embed always reads back with a `url`, never a `file_upload`. |
```json Example Embed block object theme={null}
{
//...other keys excluded
"type": "embed",
//...other keys excluded
"embed": {
"url": "https://companywebsite.com"
}
}
```
```json Attach an uploaded file to an embed theme={null}
{
"type": "embed",
"embed": {
"type": "file_upload",
"file_upload": {
"id": "43833259-72ae-404e-8441-b6577f3159b4"
}
}
}
```
Provide either `url` or `file_upload` when creating or updating an embed, not both. An embed backed by an uploaded file is returned with a temporary, signed `embed.url`. The link expires, so fetch the block again from the API to get a fresh `url` instead of caching it.
**Differences in embed blocks between the Notion app and the API**
The Notion app uses a 3rd-party service, iFramely, to validate and request metadata for embeds given a URL. This works well in a web app because Notion can kick off an asynchronous request for URL information, which might take seconds or longer to complete, and then update the block with the metadata in the UI after receiving a response from iFramely.
We chose not to call iFramely when creating embed blocks in the API because the API needs to be able to return faster than the UI, and because the response from iFramely could actually cause us to change the block type. This would result in a slow and potentially confusing experience as the block in the response would not match the block sent in the request.
The result is that embed blocks created using the API may not look exactly like their counterparts created in the Notion app.
Vimeo video links can be embedded in a Notion page using the API using the embed block type.
For example, the following object can be passed to the [Append block children endpoint](/reference/patch-block-children):
```json JSON theme={null}
{
"children": [
{
"embed": {
"url": "https://player.vimeo.com/video/226053498?h=a1599a8ee9"
}
}
]
}
```
For other video sources, see [Supported video types](/reference/block#supported-video-types).
#### HTML blocks
An embed backed by an uploaded HTML file is an **HTML block**: the Notion app renders the file's contents interactively in a sandboxed iframe instead of linking out to it. This is the same block the app creates with the `/html` command and that agents create through [Notion MCP](/guides/mcp/mcp-supported-tools).
To create an HTML block with the API:
1. Upload a file with an `.html` or `.htm` extension using the [File Upload API](/reference/file-upload).
2. Attach it to an embed block via `embed.file_upload` when creating or updating the block.
```json Create an HTML block theme={null}
{
"type": "embed",
"embed": {
"type": "file_upload",
"file_upload": {
"id": "43833259-72ae-404e-8441-b6577f3159b4"
}
}
}
```
Retrieving an HTML block returns a temporary, signed `embed.url` for the underlying HTML file, like any other file-backed embed. Updating the embed to point at a non-HTML file or at a `url` turns it back into a regular embed.
### Equation
Equation block objects are represented as children of [paragraph](/reference/block#paragraph) blocks. They are nested within a [rich text object](/reference/rich-text) and contain the following information within the `equation` property:
| Field | Type | Description |
| :----------- | :------- | :------------------------- |
| `expression` | `string` | A KaTeX compatible string. |
```json Example Equation object theme={null}
{
//...other keys excluded
"type": "equation",
//...other keys excluded
"equation": {
"expression": "e=mc^2"
}
}
```
### File
File block objects contain the following information within the `file` property:
| Field | Type | Description |
| :------------ | :-------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `caption` | `array` of [rich text objects](/reference/rich-text) | The caption of the file block. |
| `type` | One of:
- `"file"` - `"external"` - `"file_upload"` | Type of file. This enum value indicates which of the following three objects are populated. |
| `file` | [Notion-hosted file object](/reference/file-object#notion-hosted-files-type-file) | A file object that details information about the file contained in the block: a temporary download `url` and `expiry_time`. After the `expiry_time`, fetch the block again from the API to get a new `url`.
Only valid as a parameter if copied verbatim from the `file` field of a recent block API response from Notion. To attach a file, provide a `type` of `file_upload` instead. |
| `external` | [External file object](/reference/file-object#external-files-type-external) | An object with a `url` property, identifying a publicly accessible URL. |
| `file_upload` | [File upload object](/reference/file-upload) | An object with the `id` of a [FileUpload](/reference/file-upload) to attach to the block. After attaching, the API response responds with a type of `file`, not `file_upload`, so your connection can access a download `url`. |
| `name` | `string` | The name of the file, as shown in the Notion UI. Note that the UI may auto-append `.pdf` or other extensions.
When attaching a `file_upload`, the `name` parameter is not required. |
```json Example File block theme={null}
{
// ... other keys excluded
"type": "file",
// ... other keys excluded
"file": {
"caption": [],
"type": "external",
"external": {
"url": "https://companywebsite.com/files/doc.txt"
},
"name": "doc.txt"
}
}
```
### Headings
All heading block objects, `heading_1`, `heading_2`, `heading_3`, and `heading_4`, contain the following information within their corresponding objects:
| Field | Type | Description |
| :-------------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text of the heading. |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
| `is_toggleable` | `boolean` | Whether or not the heading block is a toggle heading or not. If `true`, then the heading block toggles and can support children. If `false`, then the heading block is a static heading block. |
```json Example Heading 1 block object theme={null}
{
//...other keys excluded
"type": "heading_1",
//...other keys excluded
"heading_1": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
}],
"color": "default",
"is_toggleable": false
}
}
```
```json Example Heading 2 block object theme={null}
{
//...other keys excluded
"type": "heading_2",
//...other keys excluded
"heading_2": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
}],
"color": "default",
"is_toggleable": false
}
}
```
```json Example Heading 3 block object theme={null}
{
//...other keys excluded
"type": "heading_3",
//...other keys excluded
"heading_3": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
}],
"color": "default",
"is_toggleable": false
}
}
```
```json Example Heading 4 block object theme={null}
{
//...other keys excluded
"type": "heading_4",
//...other keys excluded
"heading_4": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
}],
"color": "default",
"is_toggleable": false
}
}
```
### Image
Image block objects contain a [file object](/reference/file-object) detailing information about the image.
```json Example Image block object theme={null}
{
// ... other keys excluded
"type": "image",
// ... other keys excluded
"image": {
"type": "external",
"external": {
"url": "https://website.domain/images/image.png"
}
}
}
```
#### Supported external image types
The image must be directly hosted. In other words, the `url` cannot point to a service that retrieves the image. The following image types are supported:
* `.bmp`
* `.gif`
* `.heic`
* `.jpeg`
* `.jpg`
* `.png`
* `.svg`
* `.tif`
* `.tiff`
#### Supported file upload types
See the [file upload reference](/reference/file-upload) for a list of supported file extensions and content types when attaching a File Upload to a block.
Image blocks only support file types in the "image" section of the table.
### Link Preview
[Link Preview](/guides/link-previews/introduction) block objects contain the originally pasted `url`:
```json Example Link preview block object theme={null}
{
//...other keys excluded
"type": "link_preview",
//...other keys excluded
"link_preview": {
"url": "https://github.com/example/example-repo/pull/1234"
}
}
```
The `link_preview` block can only be returned as part of a response. The API does not support creating or appending `link_preview` blocks.
### Meeting notes
In API version `2026-03-11`, the `transcription` block type was renamed to `meeting_notes`. If you're using an older API version, see [`transcription`](/reference/block#transcription) below.
Meeting notes block objects represent [AI meeting notes](https://www.notion.com/help/ai-meeting-notes) in the Notion UI. The block surfaces metadata about a meeting — its title, lifecycle status, calendar event details, and recording window — along with pointers to child blocks that hold the generated summary, notes, and transcript content.
Meeting notes blocks cannot be created or updated with the general block endpoints. Use [Create a meeting note](/reference/create-meeting-note) to create one from uploaded media. Use the child block IDs returned in the `children` field to fetch each section with the [Retrieve a block](/reference/retrieve-a-block) or [Retrieve block children](/reference/get-block-children) endpoints.
Meeting notes block objects contain the following information within the `meeting_notes` (or `transcription` in older API versions) property:
| Field | Type | Description |
| :--------------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title` | `array` of [rich text objects](/reference/rich-text) | The display name of the meeting notes session. |
| `status` | `string` (enum) | The lifecycle status of the transcription. Possible values are: `"transcription_not_started"`, `"transcription_paused"`, `"transcription_in_progress"`, `"transcription_failed"`, `"summary_in_progress"`, `"notes_ready"`. |
| `children` | `object` | Pointers to the related content blocks. Contains `summary_block_id`, `notes_block_id`, and `transcript_block_id` (each an optional UUID string). |
| `calendar_event` | `object` | Calendar metadata for the meeting. Contains `start_time` and `end_time` (ISO 8601 timestamps) and `attendees` (an optional array of user IDs). |
| `recording` | `object` | The recording time window. Contains `start_time` and `end_time` (ISO 8601 timestamps). |
All top-level fields are optional and only present when the corresponding data is available.
```json Example Meeting notes block object (2026-03-11) expandable theme={null}
{
"object": "block",
"id": "d7b3c8f4-9e6e-4c1a-b5b8-2c0f4a0c5b8e",
"type": "meeting_notes",
//...other keys excluded
"meeting_notes": {
"title": [
{
"type": "text",
"text": {
"content": "Team Sync",
"link": null
},
"plain_text": "Team Sync",
"href": null
}
],
"status": "notes_ready",
"children": {
"summary_block_id": "a1b2c3d4-5678-9abc-def0-1234567890ab",
"notes_block_id": "b2c3d4e5-6789-abcd-ef01-234567890abc",
"transcript_block_id": "c3d4e5f6-789a-bcde-f012-34567890abcd"
},
"calendar_event": {
"attendees": ["ee5f0f84-409a-440f-983a-a5315961c6e4"],
"start_time": "2026-02-24T10:00:00.000Z",
"end_time": "2026-02-24T10:45:00.000Z"
},
"recording": {
"start_time": "2026-02-24T10:00:00.000Z",
"end_time": "2026-02-24T10:45:00.000Z"
}
}
}
```
### Mention
A mention block object is a child of a [rich text object](/reference/rich-text) that is nested within a [paragraph block object](/reference/block#paragraph). This block type represents any `@` tag in the Notion UI, for a user, date, Notion page, Notion database, or a miniaturized version of a [Link Preview](/reference/unfurl-attribute-object).
A mention block object contains the following fields:
| Field | Type | Description |
| :------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------- |
| `type` | `"database"`
`"date"`
`"link_preview"`
`"page"`
`"user"` | A constant string representing the type of the mention. |
| `"database"`
`"date"`
`"link_preview"`
`"page"`
`"user"` | `object` | An object with type-specific information about the mention. |
```json Example Mention object theme={null}
{
//...other keys excluded
"type": "page",
"page": {
"id": "3c612f56-fdd0-4a30-a4d6-bda7d7426309"
}
}
```
### Numbered list item
Numbered list item block objects contain the following information within the `numbered_list_item` property:
| Field | Type | Description |
| :----------------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text displayed in the `numbered_list_item` block. |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
| `list_start_index` | `integer` (optional) | The start index of a list, used to represent a list that doesn't start at 1. Only present on the first item of a list. |
| `list_format` | `string` (enum) (optional) | The type of list format. Possible values are: `"numbers"`, `"letters"`, and `"roman"`. Only present on the first item of a list. |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks (if any) of the `numbered_list_item` block. |
```json Example Numbered list item block theme={null}
{
//...other keys excluded
"type": "numbered_list_item",
"numbered_list_item": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Finish reading the docs",
"link": null
}
}
],
"color": "default"
}
}
```
### Paragraph
Paragraph block objects contain the following information within the `paragraph` property:
| Field | Type | Description |
| :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text displayed in the paragraph block. |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
| `icon` | [emoji](/reference/emoji-and-icon#emoji), [custom emoji](/reference/emoji-and-icon#custom-emoji), [icon](/reference/emoji-and-icon#icon), or [file object](/reference/file-object) (nullable) | An optional icon for the paragraph. Can only be set when the paragraph is a direct child of a [tab](/reference/block#tab) block. |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks (if any) of the `paragraph` block. |
```json Example Paragraph block object theme={null}
{
//...other keys excluded
"type": "paragraph",
//...other keys excluded
"paragraph": {
"rich_text": [{
"type": "text",
"text": {
"content": "Lacinato kale",
"link": null
}
}],
"color": "default",
"icon": null
}
```
```json Example Paragraph block object with a child Mention block object expandable theme={null}
{
//...other keys excluded
"type": "paragraph",
"paragraph":{
"rich_text": [
{
"type": "mention",
"mention": {
"type": "date",
"date": {
"start": "2023-03-01",
"end": null,
"time_zone": null
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "2023-03-01",
"href": null
},
{
"type": "text",
"text": {
"content": " ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": " ",
"href": null
}
],
"color": "default"
}
}
```
### PDF
A PDF block object represents a PDF that has been embedded within a Notion page. It contains the following fields:
| Property | Type | Description |
| :---------------------------------- | :-------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `caption` | `array` of [rich text objects](/reference/rich-text) | A caption, if provided, for the PDF block. |
| `type` | One of:
- `"file"` - `"external"` - `"file_upload"` | A constant string representing the type of PDF. `file` indicates a Notion-hosted file, and `external` represents a third-party link. `file_upload` is only valid when providing parameters to attach a [File Upload](/reference/file-upload) to a PDF block. |
| `external` \|`file` \|`file_upload` | [file object](/reference/file-object) | An object containing type-specific information about the PDF. |
```json JSON theme={null}
{
//...other keys excluded
"type": "pdf",
//...other keys excluded
"pdf": {
"type": "external",
"external": {
"url": "https://website.domain/files/doc.pdf"
}
}
}
```
#### Supported file upload types
See the [file upload reference](/reference/file-upload) for a list of supported file extensions and content types when attaching a File Upload to a block.
PDF blocks only support a type of `.pdf`.
### Quote
Quote block objects contain the following information within the `quote` property:
| Field | Type | Description |
| :---------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text displayed in the quote block. |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks, if any, of the quote block. |
```json Example Quote block theme={null}
{
//...other keys excluded
"type": "quote",
//...other keys excluded
"quote": {
"rich_text": [{
"type": "text",
"text": {
"content": "To be or not to be...",
"link": null
},
//...other keys excluded
}],
//...other keys excluded
"color": "default"
}
}
```
### Synced block
Similar to the Notion UI, there are two versions of a `synced_block` object: the original block that was created first and doesn't yet sync with anything else, and the duplicate block or blocks synced to the original.
An original synced block must be created before corresponding duplicate block or blocks can be made.
#### Original synced block
Original synced block objects contain the following information within the `synced_block` property:
| Field | Type | Description |
| :------------ | :------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- |
| `synced_from` | `null` | The value is always `null` to signify that this is an original synced block that does not refer to another block. |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks, if any, of the `synced_block` block. These blocks will be mirrored in the duplicate `synced_block`. |
```json Example Original synced block theme={null}
{
//...other keys excluded
"type": "synced_block",
"synced_block": {
"synced_from": null,
"children": [
{
"callout": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Callout in synced block"
}
}
]
}
}
]
}
}
```
#### Duplicate synced block
Duplicate synced block objects contain the following information within the `synced_from` object:
| Field | Type | Description |
| :--------- | :---------------- | :---------------------------------------------------------------------------------------------- |
| `type` | `string` (enum) | The type of the synced from object.
Possible values are:
- `"block_id"` |
| `block_id` | `string` (UUIDv4) | An identifier for the original `synced_block`. |
```json Example Duplicate synced block object theme={null}
{
//...other keys excluded
"type": "synced_block",
"synced_block": {
"synced_from": {
"block_id": "original_synced_block_id"
}
}
}
```
The API does not supported updating synced block content.
### Table
Table block objects are parent blocks for table row children. Table block objects contain the following fields within the `table` property:
| Field | Type | Description |
| :------------------ | :-------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| `table_width` | `integer` | The number of columns in the table.
**Note that this cannot be changed using the API once a table is created.** |
| `has_column_header` | `boolean` | Whether the table has a column header. If `true`, then the first row in the table appears visually distinct from the other rows. |
| `has_row_header` | `boolean` | Whether the table has a header row. If `true`, then the first column in the table appears visually distinct from the other columns. |
```json Example Table block object theme={null}
{
//...other keys excluded
"type": "table",
"table": {
"table_width": 2,
"has_column_header": false,
"has_row_header": false
}
}
```
**`table_width` can only be set when the table is first created.**
Note that the number of columns in a table can only be set when the table is first created. Calls to the Update block endpoint to update `table_width` fail.
#### Table rows
Follow these steps to fetch the `table_row`s of a `table`:
Get the `table` ID from a query to [Retrieve block children](/reference/get-block-children) for the parent page.
Get the `table_rows` from a query to Retrieve block children for the `table`.
A `table_row` block object contains the following fields within the `table_row` property:
| Property | Type | Description |
| :------- | :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------- |
| `cells` | `array` of array of [rich text objects](/reference/rich-text) | An array of cell contents in horizontal display order. Each cell is an array of rich text objects. |
```json Example Table row block object expandable theme={null}
{
//...other keys excluded
"type": "table_row",
"table_row": {
"cells": [
[
{
"type": "text",
"text": {
"content": "column 1 content",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "column 1 content",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "column 2 content",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "column 2 content",
"href": null
}
],
[
{
"type": "text",
"text": {
"content": "column 3 content",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "column 3 content",
"href": null
}
]
]
}
}
```
When creating a table block using the [Append block children](/reference/patch-block-children) endpoint, the `table` must have at least one `table_row` whose `cells` array has the same length as the `table_width`.
### Table of contents
Table of contents block objects contain the following information within the `table_of_contents` property:
| Property | Type | Description |
| :------- | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
```json Example Table of contents block object theme={null}
{
//...other keys excluded
"type": "table_of_contents",
"table_of_contents": {
"color": "default"
}
}
```
### Tab
Tab block objects represent a tabbed container. The `tab` property is an empty object (`{}`). Each tab within the container is a [paragraph](/reference/block#paragraph) child block — the paragraph's `rich_text` serves as the tab label, the paragraph's `icon` sets the tab icon, and the paragraph's `children` contain the tab's content.
Only `paragraph` blocks can be direct children of a tab block. When creating tabs via [Append block children](/reference/patch-block-children), provide a `tab` block with paragraph children, each containing their own nested content blocks. Each paragraph child can optionally include an `icon` to display alongside the tab label.
| Field | Type | Description |
| :-------- | :------- | :----------------------------------------------------------------------------------------------- |
| *(empty)* | `object` | The `tab` property is an empty object. Tab metadata is expressed through its paragraph children. |
```json Example Tab block object theme={null}
{
"object": "block",
"type": "tab",
"has_children": true,
//...other keys excluded
"tab": {}
}
```
```json Creating a tab block with icons theme={null}
{
"type": "tab",
"tab": {
"children": [
{
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "Overview" } }],
"icon": { "emoji": "📋" },
"children": [
{
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "Tab 1 content" } }]
}
}
]
}
},
{
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "Details" } }],
"icon": { "emoji": "🔍" },
"children": [
{
"type": "paragraph",
"paragraph": {
"rich_text": [{ "type": "text", "text": { "content": "Tab 2 content" } }]
}
}
]
}
}
]
}
}
```
### Template
**Deprecation Notice**
As of March 27, 2023 creation of template blocks will no longer be supported.
Template blocks represent [template buttons](https://www.notion.com/help/template-buttons) in the Notion UI.
Template block objects contain the following information within the `template` property:
| Field | Type | Description |
| :---------- | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text displayed in the title of the template. |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks, if any, of the template block. These blocks are duplicated when the template block is used in the UI. |
```json Example Template block object theme={null}
{
//...other keys excluded
"template": {
"rich_text": [
{
"type": "text",
"text": {
"content": "Add a new to-do",
"link": null
},
"annotations": {
//...other keys excluded
},
"plain_text": "Add a new to-do",
"href": null
}
]
}
}
```
### To do
To do block objects contain the following information within the `to_do` property:
| Field | Type | Description |
| :---------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text displayed in the To do block. |
| `checked` | `boolean` (optional) | Whether the To do is checked. |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks, if any, of the To do block. |
```json Example To do block object theme={null}
{
//...other keys excluded
"type": "to_do",
"to_do": {
"rich_text": [{
"type": "text",
"text": {
"content": "Finish Q3 goals",
"link": null
}
}],
"checked": false,
"color": "default",
"children":[{
"type": "paragraph"
// ..other keys excluded
}]
}
}
```
### Toggle blocks
Toggle block objects contain the following information within the `toggle` property:
| Field | Type | Description |
| :---------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rich_text` | `array` of [rich text objects](/reference/rich-text) | The rich text displayed in the Toggle block. |
| `color` | `string` (enum) | The color of the block. Possible values are:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` - `"orange_background"` - `"yellow"` - `"green"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background"` - `"yellow_background"` |
| `children` | `array` of [block objects](/reference/block) | The nested child blocks, if any, of the Toggle block. |
```json Toggle Block theme={null}
{
//...other keys excluded
"type": "toggle",
"toggle": {
"rich_text": [{
"type": "text",
"text": {
"content": "Additional project details",
"link": null
}
//...other keys excluded
}],
"color": "default",
"children":[{
"type": "paragraph"
// ..other keys excluded
}]
}
}
```
### Transcription
**Deprecated in `2026-03-11`** — This block type has been renamed to [`meeting_notes`](/reference/block#meeting-notes). The `transcription` type name is still returned for API versions prior to `2026-03-11`.
The `transcription` block type is identical to [`meeting_notes`](/reference/block#meeting-notes) except for the type name. In responses, `type` is `"transcription"` and the content object key is `transcription` instead of `meeting_notes`. All fields are the same — see the [meeting notes reference](/reference/block#meeting-notes) for details.
### Unsupported
When a block type is not yet supported by the API, the response includes `type` set to `"unsupported"` and an `unsupported` object with a `block_type` field. The `block_type` value is a plain string identifying the underlying internal block type (e.g., `"form"`, `"button"`, `"drive"`). This field is informational only and does not expose block content.
| Field | Type | Description |
| :----------- | :------- | :------------------------------------------------------------------------------------------------------------------------------ |
| `block_type` | `string` | The underlying block type that is not currently supported by the Notion API. Example values include: `form`, `button`, `drive`. |
```json Example Unsupported block object theme={null}
{
"object": "block",
"id": "7af38973-3787-41b3-bd75-0ed3a1edfac9",
"type": "unsupported",
//...other keys excluded
"unsupported": {
"block_type": "form"
}
}
```
The `block_type` value is a plain string, not an enum. New values may appear as Notion adds new block types. Do not rely on a fixed set of values.
### Video
Video block objects contain a [file object](/reference/file-object) detailing information about the video.
```json Example Video block object theme={null}
{
"type": "video",
//...other keys excluded
"video": {
"type": "external",
"external": {
"url": "https://companywebsite.com/files/video.mp4"
}
}
}
```
#### Supported video types
* `.amv`
* `.asf`
* `.avi`
* `.f4v`
* `.flv`
* `.gifv`
* `.mkv`
* `.mov`
* `.mpg`
* `.mpeg`
* `.mpv`
* `.mp4`
* `.m4v`
* `.qt`
* `.wmv`
* YouTube video links that include `embed` or `watch`. E.g. `https://www.youtube.com/watch?v=[id]`, `https://www.youtube.com/embed/[id]`
Vimeo video links are not currently supported by the video block type. However, they can be embedded in Notion pages using the `embed` block type. See [Embed](/reference/block#embed) for more information.
#### Supported file upload types
See the [file upload reference](/reference/file-upload) for a list of supported file extensions and content types when attaching a File Upload to a block.
Video blocks only support file types in the "video" section of the table.
# Connection capabilities
Source: https://developers.notion.com/reference/capabilities
Learn about the capabilities that control what a connection can do and see in a Notion workspace.
All connections and [personal access tokens](/guides/get-started/personal-access-tokens) have associated capabilities which enforce what they can do and see in a Notion workspace.
These capabilities when put together enforce which API endpoints a connection or token can call, and what content and user related information they are able to see.
To set your connection's capabilities see the [Authorization](/guides/get-started/authorization) guide or navigate to the Developer portal. For PATs, choose capabilities when you create the token.
**If a connection is added to a page, then the connection can access the page’s children**
When a connection receives access to a Notion page or database, it can read and write to both that resource and its children.
## Content capabilities
Content capabilities affect how a connection can interact with [database objects](/reference/database), [page objects](/reference/page), and [block objects](/reference/block) via the API. Additionally, these capabilities affect what information is exposed to a connection in API responses. To verify which capabilities are needed for an endpoint's desired behavior, please use the API references.
* **Read content**: This capability gives a connection access to read existing content in a Notion workspace. For example, a connection with only this capability is able to call [Retrieve a database](/reference/retrieve-a-database) , but not [Update database](/reference/update-a-database).
* **Update content**: This capability gives a connection permission to update existing content in a Notion workspace. For example, a connection with only this capability is able to call the [Update page](/reference/patch-page) endpoint, but is not able to create new pages.
* **Insert content**: This capability gives a connection permission to create new content in a Notion workspace. This capability does not give the connection access to read full objects. For example a connection with only this capability is able to [Create a page](/reference/post-page) but is not able to update existing pages.
*It is possible for a connection to have any combination of these content capabilities.*
## Comment capabilities
Comment capabilities dictate how a connection can interact with the [comments](/reference/comment-object) on a page or block.
* **Read comments**: This capability gives the connection permission to [read comments](/reference/list-comments) from a Notion page or block.
* **Insert comments**: This capability gives the connection permission to [insert comments](/reference/create-a-comment) in a page or in an existing discussion.
## User capabilities
A connection can request different levels of user capabilities, which affect how [user objects](/reference/user) are returned from the Notion API:
* **No user information**: Selecting this option prevents a connection from requesting any information about users. User objects will not include any information about the user, including name, profile image, or their email address.
* **User information without email addresses**: Selecting this option ensures that User objects will include all information about a user, including name and profile image, but omit the email address.
* **User information with email addresses**: Selecting this option ensures that User objects will include all information about the user, including name, profile image, and their email address.
## Capability Behaviors and Best Practices
A connection's capabilities will never supersede a user's. If a user loses edit access to the page where they have added a connection, that connection will now also only have read access, regardless of the capabilities the connection was created with. Similarly, a PAT can only use the permissions of the user who created it.
For public connections, users will need to re-authenticate with a connection if the capabilities are changed in the time since the user last authenticated with the connection.
To learn more about setting your connection's capabilities refer to the [Authorization](/guides/get-started/authorization) guide.
In general, you want to request minimum capabilities that your connection needs in order to function. The fewer capabilities you request, the more likely a workspace admin will be able to install your connection.
For example:
* If your connection is solely bringing data into Notion (creating new pages, or adding blocks), your connection only needs **Insert content** capabilities.
* If your connection is reading data to export it out of Notion, your connection will only need **Read content** capabilities.
* If your connection is simply updating a property on a page or an existing block, your connection will only need **Update content** capabilities.
# Changes by version
Source: https://developers.notion.com/reference/changes-by-version
| Version | Breaking changes |
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `2026-03-11` | The `after` parameter on Append Block Children is replaced by a `position` object. The `archived` field is removed in favor of `in_trash` across all request parameters and response bodies. The `transcription` block type is renamed to `meeting_notes`.
See [Upgrading to 2026-03-11](/guides/get-started/upgrade-guide-2026-03-11) for details. |
| `2025-09-03` | `/v1/databases` APIs are re-organized into `/v1/data_sources` (for managing individual data sources under a database container) and `/v1/databases` (for managing the database container.)
Existing database IDs stay the same, but a new concept of data source IDs is introduced, and required in order to manage data source properties, to support multi-source databases (new in the Notion app as of September 2025.)
See [changelog](/page/changelog) and [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03) guide for more details. |
| `2022-06-28` | Page properties must be retrieved using the page properties endpoint.
Parents are now always direct parents; a parent field has been added to block.
Database relations have a type of `single_property` and `dual_property`.
See [changelog](/changelog/releasing-notion-version-2022-06-28) for more details. |
| `2022-02-22` | See [changelog](/changelog/releasing-notion-version-2022-02-22). |
| `2021-08-16` | The [Append block children](/reference/patch-block-children) endpoint returns a list of new [Block object](/reference/block) children instead of the parent block.
Array rollup property types changed from `file`, `text` and `person` to `files`, `rich_text` and `people`.
Property IDs are now encoded to be URL safe.
Empty number, email, select, date, and rollup properties are now returned in page responses as `null`.
[More information](/changelog/notion-version-2021-08-16) |
| `2021-05-13` | Rich text property values use the type `rich_text` instead of `text`.
[Migration details](/changelog/unversioned-requests-no-longer-accepted) |
# Comment attachment
Source: https://developers.notion.com/reference/comment-attachment
The Comment Attachment object represents [files](/reference/file-object) that have been attached to a [Comment](/reference/comment-object).
Comments can currently support up to 3 attachments.
## Request format (input)
### Object properties
After following the [Working with files and media](/guides/data-apis/working-with-files-and-media) guide, provide an array of objects under the `attachments` parameter in the [Create comment](/reference/create-a-comment) API, each containing the following properties:
| Parameter | Type | Description | Example value |
| :--------------- | :------------------ | :-------------------------------------------------------------------------- | :--------------------------------------- |
| `file_upload_id` | `string` (UUID) | ID of a [File Upload](/reference/file-upload) with a status of `"uploaded"` | `"2e2cdb8b-9897-4a6c-a935-82922b1cfb87"` |
| `type` | `string` (optional) | Possible type values are:`"file_upload"` | `"file_upload"` |
Example Create Comment request:
```json API request theme={null}
{
"parent": {
"page_id": "d0a1ffaf-a4d8-4acf-a1ed-abae6e110418"
},
"rich_text": [
{
"text": {"content": "Thanks for the helpful page!"}
},
],
"attachments": {
"file_upload_id": "2e2cdb8b-9897-4a6c-a935-82922b1cfb87"
}
}
```
In the Notion app, when viewing a comment uploaded using the API, the user experience is automatically customized based on the detected category of the file upload. For example, uploading a `.png` file displays your attachment as an inline image instead of a regular file download block.
## Response format (output)
### Object properties
The response of Comment APIs like [Create comment](/reference/create-a-comment) contains `attachments` with the following fields:
| Field | Type | Description | Example value |
| :--------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------- |
| `category` | `string` (enum) | The category of this attachment. Possible type values are: `"audio"`, `"image"`, `"pdf"`, `"productivity"`, and `"video"` | `"audio"` |
| `file` | `object` | A [file object](/reference/file-object#notion-hosted-files-type-file) containing type-specific configuration. | `{"url": ", "expiry_time": "2025-06-10T21:26:03.070Z"}` |
Example attachment object in Create Comment response:
```json Comment Attachment Response theme={null}
{
"category": "video",
"file": {
"url": "https://s3.us-west-2.amazonaws.com/...",
"expiry_time": "2025-06-10T21:26:03.070Z"
}
}
```
The `file.url` is a temporary download link generated at the time of retrieving a comment. See the guide on [Retrieving existing files](/guides/data-apis/retrieving-files) to learn more about accessing the files you upload.
# Comment display name
Source: https://developers.notion.com/reference/comment-display-name
The Comment Display Name object represents the author name that shows up for a comment. This overrides the default author name when specified.
## Request format (input)
### Object properties
| Parameter | Type | Description | Example value |
| :-------- | :-------------- | :----------------------------------------------------------------------------------------------------------------- | :----------------------------------- |
| `type` | `string` (enum) | Possible type values are:`"connection"`, `"user"`, or `"custom"` | `"user"` |
| `custom` | `object` | If the type is `"custom"`, include a custom object specifying the custom name`"custom": { "name": }` | `"custom": { "name": "Notion Bot" }` |
* `"connection"`: name of the [connection](/guides/get-started/overview)
* `"user"`: name of the user who authenticated the connection or token (for [public connections](/guides/get-started/overview#connection-types) and [personal access tokens](/guides/get-started/personal-access-tokens))
* `"custom"`: any custom name
Example of a Create Comment request with custom display name:
```json API request theme={null}
{
"parent": {
"page_id": "d0a1ffaf-a4d8-4acf-a1ed-abae6e110418"
},
"rich_text": [
{
"text": {
"content": "Thanks for checking us out!"
}
}
],
"display_name": {
"type": "custom",
"custom": {
"name": "Notion Bot"
}
}
}
```
## Response format (output)
### Object properties
The response of Comment APIs like [Create comment](/reference/create-a-comment) contains `attachments` with the following fields:
| Field | Type | Description | Example value |
| :-------------- | :-------------- | :--------------------------------------------------------------- | :------------- |
| `type` | `string` (enum) | Possible type values are:`"connection"`, `"user"`, or `"custom"` | `"custom"` |
| `resolved_name` | `string` | The custom display name shown in a comment | `"Notion Bot"` |
```json API Response theme={null}
{
...existing parameters omitted,
"display_name": {
"type": "custom",
"resolved_name": "Notion Bot"
}
}
```
# Comment
Source: https://developers.notion.com/reference/comment-object
The Comment object represents a comment on a Notion page or block. Comments can be viewed or created by a connection that has access to the page/block and the correct capabilities. Please see the [Capabilities guide](/reference/capabilities) for more information on setting up your connection's capabilities.
When [retrieving comments](/reference/list-comments), one or more Comment objects will be returned in the form of an array, sorted in ascending chronological order. When [adding a comment](/reference/create-a-comment) to a page or discussion, the Comment object just added will always be returned. When [updating a comment](/reference/update-a-comment), the updated Comment object will be returned. When [deleting a comment](/reference/delete-a-comment), the deleted Comment object will be returned.
```json JSON expandable theme={null}
{
"object": "comment",
"id": "7a793800-3e55-4d5e-8009-2261de026179",
"parent": {
"type": "page_id",
"page_id": "5c6a2821-6bb1-4a7e-b6e1-c50111515c3d"
},
"discussion_id": "f4be6752-a539-4da2-a8a9-c3953e13bc0b",
"created_time": "2022-07-15T21:17:00.000Z",
"last_edited_time": "2022-07-15T21:17:00.000Z",
"created_by": {
"object": "user",
"id": "e450a39e-9051-4d36-bc4e-8581611fc592"
},
"rich_text": [
{
"type": "text",
"text": {
"content": "Hello world",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Hello world",
"href": null
}
],
"attachments": [
{
"category": "image",
"file": {
"url": "https://s3.us-west-2.amazonaws.com/...",
"expiry_time": "2025-06-10T21:58:51.599Z"
}
}
],
"display_name": {
"type": "user",
"resolved_name": "Avo Cado"
}
}
```
## All comments
**Reminder: Turn on connection comment capabilities**
Connections must have read comments or insert comments capabilities in order to interact with the Comment object through the API. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
| Property | Type | Description | Example value |
| :----------------- | :-------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object` | `string` | Always `"comment"` | `"comment"` |
| `id` | `string` (UUIDv4) | Unique identifier of the comment. | `"ce18f8c6-ef2a-427f-b416-43531fc7c117"` |
| `parent` | `object` | Information about the comment's parent. See [Parent object](/reference/parent-object). Note that comments may only be parented by pages or blocks. | `{ "type": "block_id", "block_id": "5d4ca33c-d6b7-4675-93d9-84b70af45d1c" }` |
| `discussion_id` | `string` (UUIDv4) | Unique identifier of the discussion thread that the comment is associated with. See [the guide](/guides/data-apis/working-with-comments#retrieving-comments-for-a-page-or-block) for more information about discussion threads. | `"ce18f8c6-ef2a-427f-b416-43531fc7c117"` |
| `created_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this comment was created. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2022-07-15T21:46:00.000Z"` |
| `last_edited_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this comment was updated. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2022-07-15T21:46:00.000Z"` |
| `created_by` | [Partial User](/reference/user) | User who created the comment. | `{ "object": "user", "id": "e450a39e-9051-4d36-bc4e-8581611fc592" }` |
| `rich_text` | [Rich text object](/reference/rich-text) | Content of the comment, which supports rich text formatting, links, and mentions. | `[ { "text": { "content": "Kale", "link": { "type": "url", "url": "https://www.healthline.com/nutrition/10-proven-benefits-of-kale" } } } ]` |
| `attachments` | [Comment Attachment](/reference/comment-attachment) | File attachments on the comment | `[ { "category": "image", "file": { "url": "https://s3.us-west-2.amazonaws.com/9bc6c6e0-32b8-4d55-8c12-3ae931f43a01/meow...", "expiry_time": "2025-06-10T21:58:51.599Z" } } ]` |
| `display_name` | [Comment Display Name](/reference/comment-display-name) | Custom display name on comment | `"display_name": { "type": "custom", "resolved_name": "automated response" }` |
# Complete a file upload
Source: https://developers.notion.com/reference/complete-file-upload
post /v1/file_uploads/{file_upload_id}/complete
Use this API to finalize a `mode=multi_part` [file upload](/reference/file-upload) after all of the parts have been sent successfully.
# Create comment
Source: https://developers.notion.com/reference/create-a-comment
post /v1/comments
Creates a comment on a page, block, or existing discussion thread.
Returns a [comment object](/reference/comment-object) for the created comment.
Choose exactly one target for the new comment:
| Target | Body parameter | Use when |
| -------------------------- | ----------------- | --------------------------------------------------------------------------------------------- |
| Page | `parent.page_id` | Creating a comment on a page. |
| Block | `parent.block_id` | Creating a comment attached to a specific block, such as a paragraph, heading, or to-do item. |
| Existing discussion thread | `discussion_id` | Replying to an existing page, block, or selected-text discussion. |
The request body will differ slightly depending on which target is being used.
To add a new comment to a page or block, provide a `parent` object with a `page_id` or `block_id` in the body params. For example, this request creates a comment attached to a block:
```json theme={null}
{
"parent": {
"block_id": "d40e767c-d7af-4b18-a86d-55c61f1e39a4"
},
"rich_text": [
{
"text": {
"content": "This comment is attached to a block."
}
}
]
}
```
To add a new comment to an existing discussion thread, provide a `discussion_id` string in the body params.
The public API can create comments attached to whole blocks with `parent.block_id`. It does not support creating a new discussion anchored to a selected range of text inside a block. To reply to an existing selected-text discussion, use `discussion_id`.
**Exactly one of `parent.page_id`, `parent.block_id`, or `discussion_id` must be provided.**
### Comment body format
The comment body can be provided in one of two formats:
* **`rich_text`**: An array of [rich text objects](/reference/rich-text) that represent the content of the comment.
* **`markdown`**: A Markdown string. Comment Markdown supports inline formatting only (bold, italic, strikethrough, inline code, links), inline equations, and mentions. Block-level Markdown such as fenced code blocks, headings, lists, tables, and blockquotes does not render as structured blocks in comments.
Exactly one of `rich_text` or `markdown` must be provided. Providing both or neither will return a validation error.
To see additional examples of creating a [page](/guides/data-apis/working-with-comments#adding-a-comment-to-a-page), [inline](/guides/data-apis/working-with-comments#adding-an-inline-comment), or [discussion](/guides/data-apis/working-with-comments#responding-to-a-discussion-thread) comment and to learn more about comments in Notion, see the [Working with comments](/guides/data-apis/working-with-comments) guide.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**Reminder: Turn on connection comment capabilities**
Connection capabilities for reading and inserting comments are off by default.
This endpoint requires a connection to have insert comment capabilities. Attempting to call this endpoint without insert comment capabilities will return an HTTP response with a 403 status code.
For more information on connection capabilities, see the [capabilities guide](/reference/capabilities). To update your connection settings, visit the Developer portal.
# Create a data source
Source: https://developers.notion.com/reference/create-a-data-source
post /v1/data_sources
Use this API to add an additional [data source](/reference/data-source) to an existing [database](/reference/database). The `properties` follow the [same structure](/reference/property-object) as the initial schema passed to `initial_data_source[properties]` in the [Create a database](/reference/create-database) API, but can be managed independently of the `properties` of any sibling data sources.
A standard "table" view is created alongside the new data source. To customize database views, use the Notion app. Managing views is not currently supported in the API.
# Create a database
Source: https://developers.notion.com/reference/create-a-database
post /v1/databases
**Deprecated as of version 2025-09-03**
This page describes the API for versions up to and including `2022-06-28`. In the new `2025-09-03` version, the concepts of databases and data sources were split up, as described in [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
Refer to the new APIs instead:
* [Create a database](/reference/create-database)
* [Create a data source](/reference/create-a-data-source)
Creates a database as a subpage in the specified parent page, with the specified `properties` schema. Currently, the parent of a new database must be a Notion page or a [wiki database](https://www.notion.com/help/wikis-and-verified-pages).
**Connection capabilities**
This endpoint requires a connection to have insert content capabilities. Attempting to call this API without insert content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 if the specified parent page does not exist, or if the connection does not have access to the parent page.
Returns a 400 if the request is incorrectly formatted, or a 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Create a token
Source: https://developers.notion.com/reference/create-a-token
post /v1/oauth/token
Creates an access token that a third-party service can use to authenticate with Notion.
For step-by-step instructions on how to use this endpoint to create a public connection, check out the [Authorization guide](/guides/get-started/authorization#public-connection-auth-flow-set-up).
**Redirect URI requirements for public connections**
The `redirect_uri` is a *required* field in the request body for this endpoint if:
* the `redirect_uri` query parameter was set in the [Authorization URL](/guides/get-started/authorization#step-1-navigate-the-user-to-the-connection’s-authorization-url) provided to users, *or*;
* there are more than one `redirect_uri`s included in the connection's settings under **OAuth Domain & URIs**.
In most cases, the `redirect_uri` field is required.
This field is not allowed in the request body if:
* there is one `redirect_uri` included in the connection's settings under **OAuth Domain & URIs**, *and* the `redirect_uri` query parameter was not included in the Authorization URL.
Learn more in the public connection section of the [Authorization Guide](/guides/get-started/authorization#public-connection-auth-flow-set-up).
*Note: Each Public API endpoint can return several possible error codes. To see a full description of each type of error code, see the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation.*
# Create a database
Source: https://developers.notion.com/reference/create-database
post /v1/databases
Create a database and its initial data source.
Creates a database as a subpage in the specified parent page, or as a private page at the workspace level, with the specified `properties` schema set on its `initial_data_source`. Currently, the `parent` of a new database must be a Notion page (`page_id` type) or a [wiki database](https://www.notion.com/help/wikis-and-verified-pages).
Use this endpoint to create a database, its first [data source](/reference/data-source), and its first table view, all in one API call. Then, if you want to add a second data source, use the [Create a data source](/reference/create-a-data-source) API with a version of at least `2025-09-03`, and provide the `database_id` as the `id` returned by the database create response.
For a complete reference on what properties are available, see [Data source properties](/reference/property-object). After creating the database, to update one of its child data sources' properties, use the [Update a data source](/reference/update-a-data-source) API.
**Connection capabilities**
> This endpoint requires a connection to have insert content capabilities. Attempting to call this API without insert content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
> Returns a 404 if the specified parent page does not exist, or if the connection does not have access to the parent page.
> Returns a 400 if the request is incorrectly formatted, or a 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Create a file upload
Source: https://developers.notion.com/reference/create-file
post /v1/file_uploads
Use this API to initiate the process of [uploading a file](/guides/data-apis/working-with-files-and-media) to your Notion workspace.
For a successful request, the response is a [File Upload](/reference/file-upload) object with a `status` of `"pending"`.
The maximum allowed length of `filename` string is 900 bytes, including any file extension included in the file name or inferred based on the `content_type`. However, we recommend using shorter names for performance and easier file management and lookup using the [List file uploads](/reference/list-file-uploads) API.
# Create a meeting note
Source: https://developers.notion.com/reference/create-meeting-note
post /v1/blocks/meeting_notes
Creates AI meeting notes from an uploaded audio or video file.
Creates a [meeting notes block](/reference/block#meeting-notes) and begins processing its source media. The response includes the ID of the created block. A full block response also includes its current processing status.
Source types allow you to directly attach a file upload, or create a meeting note block from an existing audio or video file block:
| Source | Body parameters | Behavior |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| Completed file upload | Set `source.type` to `file_upload`, provide `source.file_upload_id`, set `parent.type` to `page_id`, and provide `parent.page_id`. | Creates the meeting notes block in the parent page. |
| Existing block | Set `source.type` to `block` and provide `source.block_id` for an audio, video, or file block backed by a Notion-hosted audio or video upload. Omit `parent`. | Creates the meeting notes block next to the source block. |
For a file upload source, upload the media and wait until the [file upload](/reference/file-upload) has a status of `uploaded` before calling this endpoint. See [Uploading small files](/guides/data-apis/uploading-small-files) for the complete upload flow.
For a `.m4a` audio file, use the `audio/mp4` content type. WebM audio is not supported.
### Processing status
Processing continues asynchronously after the endpoint returns. Use [Retrieve a block](/reference/retrieve-a-block) with the returned block ID and check `meeting_notes.status`. When summary generation is enabled, wait for `notes_ready` before retrieving the generated summary and notes from the IDs in `meeting_notes.children`.
By default, Notion starts summary generation after transcription. Set `options.kickoff_summary` to `false` to transcribe the media without generating a summary.
If you provide a `title` and enable summary generation, treat the title as provisional. Notion may replace it when processing completes.
This endpoint is not idempotent. Retrying a request may create duplicate meeting notes blocks.
### Requirements
The integration must have the **Insert content** capability. **Read content** is also required to use an existing block as the source or receive the full meeting notes block in the response. Without **Read content**, a request using a file upload source returns only the created block's `object` and `id`.
The user associated with the integration must also have access to AI meeting notes. See the [capabilities guide](/reference/capabilities) and [Notion pricing](https://www.notion.com/pricing).
### Errors
Returns a 400 HTTP response if AI meeting notes are not available to the integration's user or if the request contains an invalid source, parent, or processing option.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) for more information.
# Create a view
Source: https://developers.notion.com/reference/create-view
post /v1/views
Create a new view on a database or add a widget view to a dashboard.
For a successful request, the response is a [View](/reference/view) object.
Provide exactly one of the following to specify where the view is created:
* `database_id` — create a view directly on an existing database.
* `view_id` — add a widget view inside an existing dashboard view.
* `create_database` — create a new linked database block on a page and add the view to it.
**Connection capabilities**
This endpoint requires a connection to have insert content and update content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the database or view doesn't exist, or if the connection doesn't have access.
Returns a 400 HTTP response if more than one of `database_id`, `view_id`, or `create_database` is provided, or if none is provided.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Create a view query
Source: https://developers.notion.com/reference/create-view-query
post /v1/views/{view_id}/queries
Execute a view's query and return the first page of results.
Executes the view's filter and sort configuration against its data source, caches the full result set, and returns the first page of page references along with a `query_id` for [paginating through results](/reference/get-view-query-results).
Cached results expire after 15 minutes. Use the `expires_at` field to check when the cache will be invalidated.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Pagination limit
This endpoint caches up to **10,000 results** per query. If the view's filter and sort configuration matches more rows than this limit, the cache will be truncated and the response will include:
```json theme={null}
{
"request_status": {
"type": "incomplete",
"incomplete_reason": "query_result_limit_reached"
}
}
```
When `request_status.type` is `"incomplete"`, the `total_count` reflects only the truncated cache size (not the full matching row count), and subsequent [paginated requests](/reference/get-view-query-results) will stop once the cache is exhausted.
To work around this limit:
* Narrow the view's filter and sort configuration via [Update a view](/reference/update-a-view) (for example, filter by `last_edited_time` to only include recently changed rows).
* Set up [connection webhooks](/reference/webhooks) to detect changes in real time instead of polling this endpoint.
**Incremental sync via webhooks**
If your connection runs this endpoint on a recurring schedule to detect changes, consider switching to [connection webhooks](/reference/webhooks) for incremental sync. Webhooks notify your connection when rows change, removing the need to re-query the view and avoiding the pagination depth limit entirely.
### Errors
Returns a 404 HTTP response if the view doesn't exist, or if the connection doesn't have access.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Data source
Source: https://developers.notion.com/reference/data-source
Learn more about Notion's data source object.
**Data sources** are the individual tables of data that live under a Notion database. [Pages](/reference/page) are the items (or children) in a data source. [Page property values](/reference/page-property-values) must conform to the [property objects](/reference/property-object) laid out in the parent data source object.
As of API version `2025-09-03`, there's a suite of APIs for managing data sources:
* [Create a data source](/reference/create-a-data-source): add an additional data source for an existing [Database](/reference/database)
* [Update a data source](/reference/update-a-data-source): update attributes, such as the `properties`, of a data source
* [Retrieve a data source](/reference/retrieve-a-data-source)
* [Query a data source](/reference/query-a-data-source)
## Object fields
Properties marked with an asterisk (\*) are available to connections with any capabilities. Other properties require read content capabilities in order to be returned from the Notion API. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
| Field | Type | Description | Example value |
| :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object`\* | `string` | Always `"data_source"`. | `"data_source"` |
| `id`\* | `string` (UUID) | Unique identifier for the data source. | `"2f26ee68-df30-4251-aad4-8ddc420cba3d"` |
| `properties`\* | `object` | Schema of properties for the data source as they appear in Notion.
`key` string The name of the property as it appears in Notion.
`value` object A [Property object](/reference/property-object). | |
| `parent` | `object` | Information about the data source's immediate parent. Most data sources are parented by a database (`type: "database_id"`). Some externally synced data sources can be parented by another data source (`type: "data_source_id"`) and include the containing `database_id`. See [Parent object](/reference/parent-object). | `{"type": "database_id", "database_id": "842a0286-cef0-46a8-abba-eac4c8ca644e"}` |
| `database_parent` | `object` | Information about the containing database's parent. See [Parent object](/reference/parent-object). | `{ "type": "page_id", "page_id": "af5f89b5-a8ff-4c56-a5e8-69797d11b9f8" }` |
| `created_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this data source was created. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T19:10:04.968Z"` |
| `created_by` | [Partial User](/reference/user) | User who created the data source. | `{"object": "user", "id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `last_edited_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this data source was updated. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T21:49:37.913Z"` |
| `last_edited_by` | [Partial User](/reference/user) | User who last edited the data source. | `{"object": "user","id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `title` | array of [rich text objects](/reference/rich-text) | Name of the data source as it appears in Notion. See [rich text object](/reference/rich-text)) for a breakdown of the properties. | `[ { "type": "text", "text": { "content": "Can I create a URL property", "link": null }, "annotations": { "bold": false, "italic": false, "strikethrough": false, "underline": false, "code": false, "color": "default" }, "plain_text": "Can I create a URL property", "href": null } ]` |
| `description` | array of [rich text objects](/reference/rich-text) | Description of the data source as it appears in Notion. See [rich text object](/reference/rich-text)) for a breakdown of the properties. | |
| `icon` | [Emoji](/reference/emoji-and-icon#emoji), [Icon](/reference/emoji-and-icon#icon), [Custom emoji](/reference/emoji-and-icon#custom-emoji), or [File](/reference/file-object) | Data source icon. See [Icon](/reference/page-property-values#icon) for all supported types. | |
| `archived` | `boolean` | **Deprecated.** Use `in_trash` instead. This is an alias for `in_trash` and always returns the same value. | `false` |
| `in_trash` | `boolean` | Whether the data source has been trashed. | `false` |
**Schema limitations**
Notion recommends a max property count of **500** or a max schema size of **50KB**. Updates to database schemas that are too large will be blocked to help maintain database performance.
# Database
Source: https://developers.notion.com/reference/database
Learn more about Notion's database object.
A **database** is an object that contains one or more [data sources](/reference/create-a-data-source). Databases can either be displayed inline in the parent page (`is_inline: true`) or as a full page (`is_inline: false`). The properties (schema) of each data source under a database can be maintained independently, and each data source has its own set of rows (pages).
Individual data sources don't have permissions settings, so the set of Notion users and bots that have access to data source children is managed through **databases**.
## Object fields
**Changed as of 2025-09-03**
In September 2025, the [Data source](/reference/data-source) object was introduced, and includes the `properties` that used to exist here at the database level.
After [upgrading your API](/guides/get-started/upgrade-guide-2025-09-03) connection to `2025-09-03`, the new database object shape is displayed, including an array of child `data_sources` but **not** the data source `properties`.
| Field | Type | Description | Example value |
| :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object` | `string` | Always `"database"`. | `"database"` |
| `id` | `string` (UUID) | Unique identifier for the database. | `"2f26ee68-df30-4251-aad4-8ddc420cba3d"` |
| `data_sources` | array of data source objects | List of child data sources, each of which is a JSON object with an `id` and `name`.
Use [Retrieve a data source](/reference/retrieve-a-data-source) to get more details on the data source, including its `properties`. | `[{"id": "c174b72c-d782-432f-8dc0-b647e1c96df6", "name": "Tasks data source"}]` |
| `created_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this database was created. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T19:10:04.968Z"` |
| `created_by` | [Partial User](/reference/user) | User who created the database. | `{"object": "user","id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `last_edited_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this database was updated. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T21:49:37.913Z"` |
| `last_edited_by` | [Partial User](/reference/user) | User who last edited the database. | `{"object": "user","id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `title` | array of [rich text objects](/reference/rich-text) | Name of the database as it appears in Notion. See [rich text object](/reference/rich-text)) for a breakdown of the properties. | `"title": [ { "type": "text", "text": { "content": "Can I create a URL property", "link": null }, "annotations": { "bold": false, "italic": false, "strikethrough": false, "underline": false, "code": false, "color": "default" }, "plain_text": "Can I create a URL property", "href": null } ]` |
| `description` | array of [rich text objects](/reference/rich-text) | Description of the database as it appears in Notion. See [rich text object](/reference/rich-text)) for a breakdown of the properties. | |
| `icon` | [Emoji](/reference/emoji-and-icon#emoji), [Icon](/reference/emoji-and-icon#icon), [Custom emoji](/reference/emoji-and-icon#custom-emoji), or [File](/reference/file-object) | Database icon. See [Icon](/reference/page-property-values#icon) for all supported types. An icon set in the Notion UI is returned by both [Retrieve a database](/reference/retrieve-a-database) and [Retrieve a data source](/reference/retrieve-a-data-source). | |
| `cover` | [File object](/reference/file-object) | Page cover image. | |
| `parent` | `object` | Information about the database's parent. See [Parent object](/reference/parent-object). | `{ "type": "page_id", "page_id": "af5f89b5-a8ff-4c56-a5e8-69797d11b9f8" }` |
| `url` | `string` | The URL of the Notion database. | `"https://app.notion.com/p/668d797c76fa49349b05ad288df2d136"` |
| `archived` | `boolean` | **Deprecated.** Use `in_trash` instead. This is an alias for `in_trash` and always returns the same value. | `false` |
| `in_trash` | `boolean` | Whether the database has been trashed. | `false` |
| `is_inline` | `boolean` | Has the value `true` if the database appears in the page as an inline block. Otherwise has the value `false` if the database appears as a child page. | `false` |
| `public_url` | `string` | The public page URL if the page has been published to the web. Otherwise, `null`. | `"https://jm-testing.notion.site/p1-6df2c07bfc6b4c46815ad205d132e22d"1` |
**What’s Next**
# Delete a block
Source: https://developers.notion.com/reference/delete-a-block
delete /v1/blocks/{block_id}
Sets a [Block object](/reference/block), including page blocks, to `in_trash: true` using the ID specified. In the Notion UI application, this moves the block to the "Trash" where it can still be accessed and restored.
To restore the block with the API, use the [Update a block](/reference/update-a-block) or [Update page](/reference/patch-page) respectively.
**Connection capabilities**
This endpoint requires a connection to have update content capabilities. Attempting to call this API without update content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the block doesn't exist, or if the connection doesn't have access to the block.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information*
# Delete comment
Source: https://developers.notion.com/reference/delete-a-comment
delete /v1/comments/{comment_id}
Deletes a comment by its `comment_id`.
Returns a [comment object](/reference/comment-object) for the deleted comment.
A connection can only delete comments that it created. Attempting to delete a comment created by another user or connection will return a 404 error.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**Reminder: Turn on connection comment capabilities**
Connection capabilities for reading and inserting comments are off by default.
This endpoint requires a connection to have insert comment capabilities. Attempting to call this endpoint without insert comment capabilities will return an HTTP response with a 403 status code.
For more information on connection capabilities, see the [capabilities guide](/reference/capabilities). To update your connection settings, visit the Developer portal.
# Delete a view
Source: https://developers.notion.com/reference/delete-view
delete /v1/views/{view_id}
Delete a view from a database.
Deletes the specified view. The last remaining view on a database cannot be deleted — delete the database instead.
For dashboard views, deleting the dashboard also archives all of its widget views.
**Connection capabilities**
This endpoint requires a connection to have update content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the view doesn't exist, or if the connection doesn't have access.
Returns a 400 HTTP response if the view is the last view on the database.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Delete a view query
Source: https://developers.notion.com/reference/delete-view-query
delete /v1/views/{view_id}/queries/{query_id}
Delete a cached view query.
Deletes a cached view query. This is idempotent — it returns success even if the query doesn't exist or has already expired.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Emoji and icon
Source: https://developers.notion.com/reference/emoji-and-icon
Learn how to use emoji and icon objects in the Notion API.
Emoji and icon objects represent icons on pages, databases, and callout blocks. The `icon` field is a discriminated union on the `type` key:
| `type` | Read | Write | Description |
| :--------------- | :--- | :---- | :------------------------------------------------------------------------------------------------------------------------ |
| `"emoji"` | Yes | Yes | A standard emoji character. See [Emoji](/reference/emoji-and-icon#emoji). |
| `"custom_emoji"` | Yes | Yes | A workspace custom emoji, referenced by `id`. See [Custom emoji](/reference/emoji-and-icon#custom-emoji). |
| `"icon"` | Yes | Yes | A native Notion icon with `name` and `color`. See [Icon](/reference/emoji-and-icon#icon). |
| `"external"` | Yes | Yes | An externally hosted image URL. See [File object](/reference/file-object). |
| `"file"` | Yes | No | A Notion-hosted file (uploaded via the UI). Returned in responses only. See [File object](/reference/file-object). |
| `"file_upload"` | No | Yes | A file uploaded via the [File Upload API](/reference/file-upload). Write-only. See [File object](/reference/file-object). |
The read/write columns above apply to **page** and **database** icons. Callout block icons support all types except `file_upload` on write — use `emoji`, `external`, `custom_emoji`, or `icon` instead.
***
## Emoji
```json Example emoji object theme={null}
{
"type": "emoji",
"emoji": "😻"
}
```
| Field | Type | Description | Example value |
| :------ | :-------- | :------------------------------------------------------------- | :------------ |
| `type` | `"emoji"` | The constant string `"emoji"` that represents the object type. | `"emoji"` |
| `emoji` | `string` | The emoji character. | `"😻"` |
### Example: set a page icon via the [Create a page](/reference/post-page) endpoint
```bash cURL theme={null}
curl 'https://api.notion.com/v1/pages' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
--data '{
"parent": {
"page_id": "13d6da822f9343fa8ec14c89b8184d5a"
},
"properties": {
"title": [
{
"type": "text",
"text": {
"content": "A page with an avocado icon",
"link": null
}
}
]
},
"icon": {
"type": "emoji",
"emoji": "🥑"
}
}'
```
### Example: set a page icon via the [Update page](/reference/patch-page) endpoint
```bash cURL theme={null}
curl https://api.notion.com/v1/pages/60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
-X PATCH \
--data '{
"icon": {
"type": "emoji",
"emoji": "🥨"
}
}'
```
***
## Custom emoji
Custom emojis are icons uploaded and managed in your workspace. Use the [List custom emojis](/reference/list-custom-emojis) endpoint to retrieve them.
| Field | Type | Description | Example value |
| :------------- | :--------------- | :-------------------------------------------------------------------- | :--------------- |
| `type` | `"custom_emoji"` | The constant string `"custom_emoji"` that represents the object type. | `"custom_emoji"` |
| `custom_emoji` | `object` | Object containing `id`, `name`, and `url`. | |
### Example: custom emoji in a page icon response
```json JSON theme={null}
{
"icon": {
"type": "custom_emoji",
"custom_emoji": {
"id": "45ce454c-d427-4f53-9489-e5d0f3d1db6b",
"name": "bufo",
"url": "https://s3-us-west-2.amazonaws.com/public.notion-static.com/865e85fc-7442-44d3-b323-9b03a2111720/3c6796979c50f4aa.png"
}
}
}
```
### Example: inline custom emoji in rich text
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "custom_emoji",
"custom_emoji": {
"id": "45ce454c-d427-4f53-9489-e5d0f3d1db6b",
"name": "bufo",
"url": "https://s3-us-west-2.amazonaws.com/public.notion-static.com/865e85fc-7442-44d3-b323-9b03a2111720/3c6796979c50f4aa.png"
}
}
}
```
### Example: set a page icon to a custom emoji
When writing, only the `id` field is required inside `custom_emoji`.
```bash cURL theme={null}
curl https://api.notion.com/v1/pages/60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
-X PATCH \
--data '{
"icon": {
"type": "custom_emoji",
"custom_emoji": {
"id": "45ce454c-d427-4f53-9489-e5d0f3d1db6b"
}
}
}'
```
### Example: look up a custom emoji by name
Use the `name` query parameter on [List custom emojis](/reference/list-custom-emojis) to resolve a custom emoji name to its ID, then use the ID to set a page icon.
```bash cURL theme={null}
curl 'https://api.notion.com/v1/custom_emojis?name=bufo' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Notion-Version: 2026-03-11"
```
```javascript TypeScript SDK theme={null}
import { Client } from "@notionhq/client"
const notion = new Client()
const response = await notion.customEmojis.list({ name: "bufo" })
const emoji = response.results[0]
// Use the ID to set a page icon
if (emoji) {
await notion.pages.update({
page_id: "60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7",
icon: {
type: "custom_emoji",
custom_emoji: { id: emoji.id },
},
})
}
```
**Example response:**
```json JSON theme={null}
{
"object": "list",
"type": "custom_emoji",
"results": [
{
"id": "45ce454c-d427-4f53-9489-e5d0f3d1db6b",
"name": "bufo",
"url": "https://s3-us-west-2.amazonaws.com/public.notion-static.com/865e85fc-7442-44d3-b323-9b03a2111720/3c6796979c50f4aa.png"
}
],
"has_more": false,
"next_cursor": null
}
```
***
## Icon
Native Notion icons are built-in icons with a name and color. They appear in the Notion UI icon picker under the "Icons" tab.
```json Example icon object theme={null}
{
"type": "icon",
"icon": {
"name": "pizza",
"color": "blue"
}
}
```
| Field | Type | Description | Example value |
| :----- | :------- | :--------------------------------------------------------------------------------------------------------- | :------------ |
| `type` | `"icon"` | The constant string `"icon"` that represents the object type. | `"icon"` |
| `icon` | `object` | An object with `name` (required) and `color` (optional, defaults to `"gray"`). See below for valid values. | |
### Icon `name`
When setting a native icon, the `name` field accepts either Notion's internal API icon name or the name shown in the Notion icon picker tooltip. Responses return the internal API icon name.
Icon picker names are case-insensitive, and spaces, underscores, and hyphens are treated equivalently. For example, `"token"`, `"star circle"`, `"star-circle"`, and `"STAR_CIRCLE"` set the same icon. `"redirect"`, `"arrow redirect heavy"`, and `"arrow_redirect_heavy"` also set the same icon.
### Icon `color`
Valid color values: `"gray"`, `"lightgray"`, `"brown"`, `"yellow"`, `"orange"`, `"green"`, `"blue"`, `"purple"`, `"pink"`, `"red"`.
If omitted when setting an icon, the color defaults to `"gray"`.
### Example: set a page icon to a native icon
```bash cURL theme={null}
curl https://api.notion.com/v1/pages/60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
-X PATCH \
--data '{
"icon": {
"type": "icon",
"icon": {
"name": "pizza",
"color": "blue"
}
}
}'
```
```javascript TypeScript SDK theme={null}
import { Client } from "@notionhq/client"
const notion = new Client()
const response = await notion.pages.update({
page_id: "60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7",
icon: {
type: "icon",
icon: { name: "pizza", color: "blue" },
},
})
```
# File
Source: https://developers.notion.com/reference/file-object
Files, images, and other media bring Notion pages to life — from rich visuals in image blocks to downloadable attachments in databases, or branded page icons and covers.
This guide introduces how file objects work in the Notion API, the different types of file sources you can work with, and how to choose the right type for your connection.
You’ll learn about:
* Files uploaded manually in the Notion UI — returned as Notion-hosted file objects (type: `file`)
* Files uploaded via API — created using the File Upload API (type: `file_upload`)
* External files — linked via a public URL (type: `external`)
## What is a file object?
In the Notion API, any media asset is represented as a file object. A file object stores metadata about the file and indicates where and how the file is hosted.
Each file object has a required type field that determines the structure of its contents:
| Field | Type | Description |
| :---------------------------------- | :-------------- | :---------------------------------------------------------------------------------------------------------------------- |
| `type` | `string` (enum) | The type of the file object. Possible type values are: `"file"`, `"file_upload"`, `"external"`. |
| `file`\|`file_upload` \| `external` | `object` | An object containing type-specific configuration. Refer to the type sections below for details on type-specific values. |
Here’s what each type looks like:
```Text javascript theme={null}
// Notion-hosted file (uploaded via UI)
{
"type": "file",
"file": {
"url": ",
"expiry_time": "2025-04-24T22:49:22.765Z"
}
}
// File uploaded via the Notion API
{
"type": "file_upload",
"file_upload": {
"id": "43833259-72ae-404e-8441-b6577f3159b4"
}
}
// External file
{
"type": "external",
"external": {
"url": "
}
}
```
### Notion-hosted files (type: `file`)
These are files that users upload manually through the Notion app — such as dragging an image into a page, adding a PDF block, or setting a page cover.
**When to use:**
* You're working with existing content in a Notion workspace
* You’re accessing files that users manually added via drag-and-drop or upload
**Tips**
* Each time you fetch a Notion-hosted file, it includes a temporary public url valid for 1 hour.
* Don’t cache or statically reference these URLs. To refresh access, re-fetch the file object.
**These corresponding file objects contain the following fields:**
| Field | Type | Description | Example value |
| :------------ | :---------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- |
| `url` | `string` | An authenticated HTTP GET URL to the file.
The URL is valid for one hour. If the link expires, send an API request to get an updated URL. | `"https://s3.us-west-2.amazonaws.com/secure.notion-static.com/9bc6c6e0-32b8-4d55-8c12-3ae931f43a01/brocolli.jpeg?..."` |
| `expiry_time` | `string` ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date time) | The date and time when the link expires. | `"2020-03-17T19:10:04.968Z"` |
**Example snippet**:
```Text JSON theme={null}
{
"type": "file",
"file": {
"url": ",
"expiry_time": "2025-04-24T22:49:22.765Z"
}
}
```
### Files uploaded in the API (type: `file_upload`)
These are files uploaded using the File Upload API. You first create a [File Upload](/reference/file-upload), send file content, and then reference it by ID to attach it.
**When to use:**
1. You want to programmatically upload files to Notion
2. You’re building automations or file-rich connections
**Tips**
* Once uploaded, you can reuse the File Upload ID to attach the same file to multiple pages or blocks
* To learn more about file uploads, view the [Working with files and media](/guides/data-apis/working-with-files-and-media) guide
**These corresponding file objects contain the following fields:**
| Field | Type | Description | Example Value |
| :---- | :--- | :--------------------------------------------------------------------------------------- | :--------------------------------------- |
| `id` | UUID | ID of a [File Upload](/reference/file-upload) object that has a `status` of `"uploaded"` | `"43833259-72ae-404e-8441-b6577f3159b4"` |
**Example snippet**:
```Text JSON theme={null}
{
"type": "file_upload",
"file_upload": {
"id": "43833259-72ae-404e-8441-b6577f3159b4"
}
}
```
## External files (type: `external`)
Use this approach if you have already hosted your files elsewhere (e.g., S3, Dropbox, CDN) and want Notion to link to them.
**When to use:**
* You have an existing CDN or media server
* You have stable, permanent URLs
* Your files are publicly accessible and don’t require authentication
* You don’t want to upload files into Notion
**How to use:**
* Pass an HTTPS URL when creating or updating file-supporting blocks or properties.
* These links never expire and will always be returned as-is in API responses.
**These corresponding file objects contain the following fields:**
| Field | Type | Description | Example value |
| :---- | :------- | :--------------------------------------- | :--------------------------------------- |
| `url` | `string` | A link to the externally hosted content. | `"https://website.domain/files/doc.txt"` |
**Example snippet**:
```Text JSON theme={null}
{
"type": "external",
"external": {
"url": "
}
}
```
**What’s Next**
📘 See the next guide for a step-by-step tutorial: Uploading a file with the Notion API.
# File Upload
Source: https://developers.notion.com/reference/file-upload
The [File Upload object](#object-properties) tracks the lifecycle of a file uploaded to Notion in the API.
**Getting started**
View [Working with files and media](/guides/data-apis/working-with-files-and-media) for a comprehensive, end-to-end guide to uploading and attaching files.
Once a file upload has a `status` of `"uploaded"`, pass its ID in a [file object](/reference/file-object#files-uploaded-in-the-api-type-file_upload) with a `type` of `file_upload` to the API to attach it to blocks, pages, and databases in a Notion workspace.
## Object properties
The response of File Upload APIs like [Retrieve a file upload](/reference/retrieve-file-upload) contains `FileUpload` objects with the following fields:
| Field | Type | Description |
| :------------------- | :-------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `object` | `"file_upload"` | |
| `id` | UUID | ID of the FileUpload. |
| `created_time` | String | ISO 8601 timestamp when the FileUpload was created. |
| `last_edited_time` | String | ISO 8601 timestamp when the FileUpload was last modified. |
| `expiry_time` | String | Nullable. ISO 8601 timestamp when the FileUpload will expire, if the API connection that created it doesn't complete the upload and attach to at least one block or other object in a workspace. |
| `status` | One of:
- `"pending"` - `"uploaded"` - `"expired"` - `"failed"` | Enum status of the file upload.
`pending` status means awaiting upload or completion of an upload.
`uploaded` status means file contents have been sent.
If the `expiry_time` is `null`, that means the file upload has already been attached to a block or other object.
`expired` and `failed` file uploads can no longer be used. `failed` is only used for FileUploads with `mode=external_url` when the import was unsuccessful. |
| `filename` | String | Nullable. Name of the file, provided during the [Create a file upload](/reference/create-file) step, or, for `single_part` uploads, can be determined from the provided filename in the form data passed to the [Send a file upload](/reference/upload-file) step.
A file extension is automatically added based on the `content_type` if the filename doesn't already have one. |
| `content_type` | String | Nullable. The MIME content type of the uploaded file. Must be provided explicitly or inferred from a `filename` that includes an extension.
For `single_part` uploads, the content type can remain `null` until the [Send a file upload](/reference/upload-file) step and inferred from the `file` parameter's content type. |
| `content_length` | Integer | Nullable. The total size of the file, in bytes. For pending `multi_part` uploads, this field is a running total based on the file segments uploaded so far and recalculated at the end during the [Complete a file upload](/reference/complete-file-upload) step. |
| `upload_url` | String | Field only included for `pending` file uploads. This is the URL to use for [sending file contents](/reference/upload-file). |
| `complete_url` | String | Field only included for `pending` file uploads created with a `mode` of `multi_part`. This is the URL to use to [complete a multi-part file upload](/reference/complete-file-upload). |
| `file_import_result` | String | Field only included for a `failed` or `uploaded` file upload created with a `mode` of `external_url`. Provides details on the success or failure of importing a file into Notion using an external URL. |
# Filter data source entries
Source: https://developers.notion.com/reference/filter-data-source-entries
Learn how to specify which pages from a data source to return.
When you [query a data source](/reference/query-a-data-source), you can send a `filter` object in the body of the request that limits the returned entries based on the specified criteria.
For example, the below query limits the response to entries where the `"Task completed"` `checkbox` property value is `true`:
```bash cURL theme={null}
curl -X POST 'https://api.notion.com/v1/data_sources/897e5a76ae524b489fdfe71f5945d1af/query' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H 'Notion-Version: 2026-03-11' \
-H "Content-Type: application/json" \
--data '{
"filter": {
"property": "Task completed",
"checkbox": {
"equals": true
}
}
}'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
// replace with your own data source ID
const dataSourceId = 'd9824bdc-8445-4327-be8b-5b47500af6ce';
const filteredRows = async () => {
const response = await notion.databases.query({
data_source_id: dataSourceId,
filter: {
property: "Task completed",
checkbox: {
equals: true
}
},
});
return response;
}
```
Filters can be chained with the `and` and `or` keys so that multiple filters are applied at the same time. (See [Query a data source](/reference/query-a-data-source) for additional examples.)
```json JSON theme={null}
{
"and": [
{
"property": "Done",
"checkbox": {
"equals": true
}
},
{
"or": [
{
"property": "Tags",
"contains": "A"
},
{
"property": "Tags",
"contains": "B"
}
]
}
]
}
```
If no filter is provided, non-archived pages in the data source will be returned with pagination. To query archived pages, set `is_archived` to `true` in the [Query a data source](/reference/query-a-data-source) request body.
## The filter object
Each `filter` object contains the following fields:
| Field | Type | Description | Example value |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------- |
| `property` | `string` | The name of the property as it appears in the data source, or the property ID. | `"Task completed"` |
| `checkbox` `date` `files` `formula` `multi_select` `number` `people` `phone_number` `relation` `rich_text` `select` `status` `timestamp` `verification` `ID` | `object` | The type-specific filter condition for the query. Only types listed in the Field column of this table are supported. Refer to [type-specific filter conditions](#type-specific-filter-conditions) for details on corresponding object values. | `"checkbox": { "equals": true }` |
```json Example checkbox filter object theme={null}
{
"filter": {
"property": "Task completed",
"checkbox": {
"equals": true
}
}
}
```
The filter object mimics the data source [filter option in the Notion UI](https://www.notion.com/help/views-filters-and-sorts).
## Type-specific filter conditions
### Checkbox
| Field | Type | Description | Example value |
| :--------------- | :-------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `equals` | `boolean` | Whether a `checkbox` property value matches the provided value exactly.
Returns or excludes all data source entries with an exact value match. | `false` |
| `does_not_equal` | `boolean` | Whether a `checkbox` property value differs from the provided value.
Returns or excludes all data source entries with a difference in values. | `true` |
```json Example checkbox filter condition theme={null}
{
"filter": {
"property": "Task completed",
"checkbox": {
"does_not_equal": true
}
}
}
```
### Date
For the `after`, `before`, `equals, on_or_before`, and `on_or_after` fields, if a date string with a time is provided, then the comparison is done with millisecond precision.
If no timezone is provided, then the timezone defaults to UTC.
A date filter condition can be used to limit `date` property value types and the [timestamp](#timestamp) property types `created_time` and `last_edited_time`.
The condition contains the below fields:
| Field | Type | Description | Example value |
| :------------- | :----------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
| `after` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns data source entries where the date property value is after the provided date. | `"2021-05-10"` `"yesterday"` |
| `before` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns data source entries where the date property value is before the provided date. | `"2021-05-10"` `"tomorrow"` |
| `equals` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns data source entries where the date property value is the provided date. | `"2021-05-10"` `"today"` |
| `is_empty` | `true` | The value to compare the date property value against.
Returns data source entries where the date property value contains no data. | `true` |
| `is_not_empty` | `true` | The value to compare the date property value against.
Returns data source entries where the date property value is not empty. | `true` |
| `next_month` | `object` (empty) | A filter that limits the results to data source entries where the date property value is within the next month. | `{}` |
| `next_week` | `object` (empty) | A filter that limits the results to data source entries where the date property value is within the next week. | `{}` |
| `next_year` | `object` (empty) | A filter that limits the results to data source entries where the date property value is within the next year. | `{}` |
| `on_or_after` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns data source entries where the date property value is on or after the provided date. | `"2021-05-10"` `"today"` |
| `on_or_before` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns data source entries where the date property value is on or before the provided date. | `"2021-05-10"` `"today"` |
| `past_month` | `object` (empty) | A filter that limits the results to data source entries where the `date` property value is within the past month. | `{}` |
| `past_week` | `object` (empty) | A filter that limits the results to data source entries where the `date` property value is within the past week. | `{}` |
| `past_year` | `object` (empty) | A filter that limits the results to data source entries where the `date` property value is within the past year. | `{}` |
| `this_week` | `object` (empty) | A filter that limits the results to data source entries where the `date` property value is this week. | `{}` |
#### Relative date values
The `after`, `before`, `equals`, `on_or_after`, and `on_or_before` fields accept the following relative date strings in addition to ISO 8601 dates. These are resolved at query time:
| Value | Description |
| :--------------------- | :---------------------------------- |
| `"today"` | The current date. |
| `"tomorrow"` | The day after the current date. |
| `"yesterday"` | The day before the current date. |
| `"one_week_ago"` | Seven days before the current date. |
| `"one_week_from_now"` | Seven days after the current date. |
| `"one_month_ago"` | One month before the current date. |
| `"one_month_from_now"` | One month after the current date. |
```json Example date filter condition theme={null}
{
"filter": {
"property": "Due date",
"date": {
"on_or_after": "2023-02-08"
}
}
}
```
```json Example relative date filter condition theme={null}
{
"filter": {
"property": "Due date",
"date": {
"on_or_after": "today"
}
}
}
```
### Files
| Field | Type | Description | Example value |
| :------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `is_empty` | `true` | Whether the files property value does not contain any data.
Returns all data source entries with an empty `files` property value. | `true` |
| `is_not_empty` | `true` | Whether the `files` property value contains data.
Returns all entries with a populated `files` property value. | `true` |
```json Example files filter condition theme={null}
{
"filter": {
"property": "Blueprint",
"files": {
"is_not_empty": true
}
}
}
```
### Formula
The primary field of the `formula` filter condition object matches the type of the formula’s result. For example, to filter a formula property that computes a `checkbox`, use a `formula` filter condition object with a `checkbox` field containing a checkbox filter condition as its value.
| Field | Type | Description | Example value |
| :--------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------- |
| `checkbox` | `object` | A [checkbox](#checkbox) filter condition to compare the formula result against.
Returns data source entries where the formula result matches the provided condition. | Refer to the [checkbox](#checkbox) filter condition. |
| `date` | `object` | A [date](#date) filter condition to compare the formula result against.
Returns data source entries where the formula result matches the provided condition. | Refer to the [date](#date) filter condition. |
| `number` | `object` | A [number](#number) filter condition to compare the formula result against.
Returns data source entries where the formula result matches the provided condition. | Refer to the [number](#number) filter condition. |
| `string` | `object` | A [rich text](#rich-text) filter condition to compare the formula result against.
Returns data source entries where the formula result matches the provided condition. | Refer to the [rich text](#rich-text) filter condition. |
```json Example formula filter condition theme={null}
{
"filter": {
"property": "One month deadline",
"formula": {
"date":{
"after": "2021-05-10"
}
}
}
}
```
### Multi-select
| Field | Type | Description | Example value |
| :----------------- | :--------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- |
| `contains` | `string` or `string[]` | The value(s) to compare the multi-select property value against.
Returns data source entries where the multi-select value matches any of the provided values. | `"Marketing"` or `["Marketing", "Sales"]` |
| `does_not_contain` | `string` or `string[]` | The value(s) to compare the multi-select property value against.
Returns data source entries where the multi-select value does not match any of the provided values. | `"Engineering"` or `["Engineering", "QA"]` |
| `is_empty` | `true` | Whether the multi-select property value is empty.
Returns data source entries where the multi-select value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the multi-select property value is not empty.
Returns data source entries where the multi-select value does contains data. | `true` |
```json Example multi-select filter condition theme={null}
{
"filter": {
"property": "Programming language",
"multi_select": {
"contains": "TypeScript"
}
}
}
```
### Number
| Field | Type | Description | Example value |
| :------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `does_not_equal` | `number` | The `number` to compare the number property value against.
Returns data source entries where the number property value differs from the provided `number`. | `42` |
| `equals` | `number` | The `number` to compare the number property value against.
Returns data source entries where the number property value is the same as the provided number. | `42` |
| `greater_than` | `number` | The `number` to compare the number property value against.
Returns data source entries where the number property value exceeds the provided `number`. | `42` |
| `greater_than_or_equal_to` | `number` | The `number` to compare the number property value against.
Returns data source entries where the number property value is equal to or exceeds the provided `number`. | `42` |
| `is_empty` | `true` | Whether the `number` property value is empty.
Returns data source entries where the number property value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the number property value is not empty.
Returns data source entries where the number property value contains data. | `true` |
| `less_than` | `number` | The `number` to compare the number property value against.
Returns data source entries where the number property value is less than the provided `number`. | `42` |
| `less_than_or_equal_to` | `number` | The `number` to compare the number property value against.
Returns data source entries where the number property value is equal to or is less than the provided `number`. | `42` |
```json Example number filter condition theme={null}
{
"filter": {
"property": "Estimated working days",
"number": {
"less_than_or_equal_to": 5
}
}
}
```
### People
You can apply a people filter condition to `people`, `created_by`, and `last_edited_by` data source property types.
The people filter condition contains the following fields:
| Field | Type | Description | Example value |
| :----------------- | :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------- |
| `contains` | `string` (UUIDv4) or `"me"` | The value to compare the people property value against.
Returns data source entries where the people property value contains the provided user. Pass a user UUID or `"me"` to match the current user (see note below). | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `does_not_contain` | `string` (UUIDv4) or `"me"` | The value to compare the people property value against.
Returns data source entries where the people property value does not contain the provided user. Pass a user UUID or `"me"` to match the current user (see note below). | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `is_empty` | `true` | Whether the people property value does not contain any data.
Returns data source entries where the people property value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the people property value contains data.
Returns data source entries where the people property value is not empty. | `true` |
```json Example people filter condition theme={null}
{
"filter": {
"property": "Last edited by",
"people": {
"contains": "c2f20311-9e54-4d11-8c79-7398424ae41e"
}
}
}
```
```json Example "me" filter condition theme={null}
{
"filter": {
"property": "Assignee",
"people": {
"contains": "me"
}
}
}
```
The `"me"` value resolves to the user associated with the token. For [public connections](/guides/get-started/overview#connection-types), this is the user who completed the OAuth flow. For [personal access tokens](/guides/get-started/personal-access-tokens), this is the user who created the token. For [internal connections](/guides/get-started/overview#connection-types), there is no associated user — `contains: "me"` returns no results and `does_not_contain: "me"` matches all entries.
### Relation
| Field | Type | Description | Example value |
| :----------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `contains` | `string` (UUIDv4) | The value to compare the relation property value against.
Returns data source entries where the relation property value contains the provided `string`. | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `does_not_contain` | `string` (UUIDv4) | The value to compare the relation property value against.
Returns entries where the relation property value does not contain the provided `string`. | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `is_empty` | `true` | Whether the relation property value does not contain data.
Returns data source entries where the relation property value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the relation property value contains data.
Returns data source entries where the property value is not empty. | `true` |
```json Example relation filter condition theme={null}
{
"filter": {
"property": "✔️ Task List",
"relation": {
"contains": "0c1f7cb280904f18924ed92965055e32"
}
}
}
```
### Rich text
| Field | Type | Description | Example value |
| :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------- |
| `contains` | `string` | The `string` to compare the text property value against.
Returns data source entries with a text property value that includes the provided `string`. | `"Moved to Q2"` |
| `does_not_contain` | `string` | The `string` to compare the text property value against.
Returns data source entries with a text property value that does not include the provided `string`. | `"Moved to Q2"` |
| `does_not_equal` | `string` | The `string` to compare the text property value against.
Returns data source entries with a text property value that does not match the provided `string`. | `"Moved to Q2"` |
| `ends_with` | `string` | The `string` to compare the text property value against.
Returns data source entries with a text property value that ends with the provided `string`. | `"Q2"` |
| `equals` | `string` | The `string` to compare the text property value against.
Returns data source entries with a text property value that matches the provided `string`. | `"Moved to Q2"` |
| `is_empty` | `true` | Whether the text property value does not contain any data.
Returns data source entries with a text property value that is empty. | `true` |
| `is_not_empty` | `true` | Whether the text property value contains any data.
Returns data source entries with a text property value that contains data. | `true` |
| `starts_with` | `string` | The `string` to compare the text property value against.
Returns data source entries with a text property value that starts with the provided `string`. | "Moved" |
```json Example rich text filter condition theme={null}
{
"filter": {
"property": "Description",
"rich_text": {
"contains": "cross-team"
}
}
}
```
### Rollup
A rollup data source property can evaluate to an array, date, or number value. The filter condition for the rollup property contains a `rollup` key and a corresponding object value that depends on the computed value type.
#### Filter conditions for `array` rollup values
| Field | Type | Description | Example value |
| :------ | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------- |
| `any` | `object` | The value to compare each rollup property value against. Can be a [filter condition](#type-specific-filter-conditions) for any other type.
Returns data source entries where the rollup property value matches the provided criteria. | `"rich_text": { "contains": "Take Fig on a walk" }` |
| `every` | `object` | The value to compare each rollup property value against. Can be a [filter condition](#type-specific-filter-conditions) for any other type.
Returns data source entries where every rollup property value matches the provided criteria. | `"rich_text": { "contains": "Take Fig on a walk" }` |
| `none` | `object` | The value to compare each rollup property value against. Can be a [filter condition](#type-specific-filter-conditions) for any other type.
Returns data source entries where no rollup property value matches the provided criteria. | `"rich_text": { "contains": "Take Fig on a walk" }` |
```json Example array rollup filter condition theme={null}
{
"filter": {
"property": "Related tasks",
"rollup": {
"any": {
"rich_text": {
"contains": "Migrate data source"
}
}
}
}
}
```
#### Filter conditions for `date` rollup values
A rollup value is stored as a `date` only if the "Earliest date", "Latest date", or "Date range" computation is selected for the property in the Notion UI.
| Field | Type | Description | Example value |
| :----- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------- |
| `date` | `object` | A [date](#date) filter condition to compare the rollup value against.
Returns data source entries where the rollup value matches the provided condition. | Refer to the [date](#date) filter condition. |
```json Example date rollup filter condition theme={null}
{
"filter": {
"property": "Parent project due date",
"rollup": {
"date": {
"on_or_before": "2023-02-08"
}
}
}
}
```
#### Filter conditions for `number` rollup values
| Field | Type | Description | Example value |
| :------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------- |
| `number` | `object` | A [number](#number) filter condition to compare the rollup value against.
Returns data source entries where the rollup value matches the provided condition. | Refer to the [number](#number) filter condition. |
```json Example number rollup filter condition theme={null}
{
"filter": {
"property": "Total estimated working days",
"rollup": {
"number": {
"does_not_equal": 42
}
}
}
}
```
### Select
| Field | Type | Description | Example value |
| :--------------- | :--------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- |
| `equals` | `string` or `string[]` | The value(s) to compare the select property value against.
Returns data source entries where the select property value matches any of the provided values. | `"This week"` or `["Low", "Medium"]` |
| `does_not_equal` | `string` or `string[]` | The value(s) to compare the select property value against.
Returns data source entries where the select property value does not match any of the provided values. | `"Backlog"` or `["Done", "Archive"]` |
| `is_empty` | `true` | Whether the select property value does not contain data.
Returns data source entries where the select property value is empty. | `true` |
| `is_not_empty` | `true` | Whether the select property value contains data.
Returns data source entries where the select property value is not empty. | `true` |
```json Example select filter condition theme={null}
{
"filter": {
"property": "Frontend framework",
"select": {
"equals": "React"
}
}
}
```
```json Example multi-value select filter condition theme={null}
{
"filter": {
"property": "Priority",
"select": {
"does_not_equal": ["Done", "Archive"]
}
}
}
```
### Status
| Field | Type | Description | Example value |
| :--------------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------- |
| equals | string or string\[] | The value(s) to compare the status property value against.
Returns data source entries where the status property value matches any of the provided values. Status group names (e.g. "To-do", "In progress", "Complete") are also accepted. | "This week" or \["To-do", "In progress"] |
| does\_not\_equal | string or string\[] | The value(s) to compare the status property value against.
Returns data source entries where the status property value does not match any of the provided values. | "Backlog" or \["Done", "Archive"] |
| is\_empty | true | Whether the status property value does not contain data.
Returns data source entries where the status property value is empty. | true |
| is\_not\_empty | true | Whether the status property value contains data.
Returns data source entries where the status property value is not empty. | true |
```json Example status filter condition theme={null}
{
"filter": {
"property": "Project status",
"status": {
"equals": "Not started"
}
}
}
```
### Timestamp
Use a timestamp filter condition to filter results based on `created_time` or `last_edited_time` values.
| Field | Type | Description | Example value |
| :------------------------------- | :------------------------------- | :----------------------------------------------------------------------- | :------------------------------------------- |
| timestamp | created\_time last\_edited\_time | A constant string representing the type of timestamp to use as a filter. | "created\_time" |
| created\_time last\_edited\_time | object | A date filter condition used to filter the specified timestamp. | Refer to the [date](#date) filter condition. |
```json Example timestamp filter condition for created_time theme={null}
{
"filter": {
"timestamp": "created_time",
"created_time": {
"on_or_before": "2022-10-13"
}
}
}
```
The `timestamp` filter condition does not require a property name. The API throws an error if you provide one.
### Verification
| Field | Type | Description | Example value |
| :----- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| status | string | The verification status being queried. Valid options are: `verified`, `expired`, `none`
Returns data source entries where the current verification status matches the queried status. | "verified" |
```json Example verification filter condition for getting verified pages theme={null}
{
"filter": {
"property": "verification",
"verification": {
"status": "verified"
}
}
}
```
### ID
Use a timestamp filter condition to filter results based on the `unique_id` value.
| Field | Type | Description | Example value |
| :------------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `does_not_equal` | `number` | The value to compare the unique\_id property value against.
Returns data source entries where the unique\_id property value differs from the provided value. | `42` |
| `equals` | `number` | The value to compare the unique\_id property value against.
Returns data source entries where the unique\_id property value is the same as the provided value. | `42` |
| `greater_than` | `number` | The value to compare the unique\_id property value against.
Returns data source entries where the unique\_id property value exceeds the provided value. | `42` |
| `greater_than_or_equal_to` | `number` | The value to compare the unique\_id property value against.
Returns data source entries where the unique\_id property value is equal to or exceeds the provided value. | `42` |
| `less_than` | `number` | The value to compare the unique\_id property value against.
Returns data source entries where the unique\_id property value is less than the provided value. | `42` |
| `less_than_or_equal_to` | `number` | The value to compare the unique\_id property value against.
Returns data source entries where the unique\_id property value is equal to or is less than the provided value. | `42` |
```json Example ID filter condition theme={null}
{
"filter": {
"and": [
{
"property": "ID",
"unique_id": {
"greater_than": 1
}
},
{
"property": "ID",
"unique_id": {
"less_than": 3
}
}
]
}
}
```
## Compound filter conditions
You can use a compound filter condition to limit the results of a data source query based on multiple conditions. This mimics filter chaining in the Notion UI.
The above filters in the Notion UI are equivalent to the following compound filter condition via the API:
```json JSON theme={null}
{
"and": [
{
"property": "Done",
"checkbox": {
"equals": true
}
},
{
"or": [
{
"property": "Tags",
"contains": "A"
},
{
"property": "Tags",
"contains": "B"
}
]
}
]
}
```
A compound filter condition contains an `and` or `or` key with a value that is an array of filter objects or nested compound filter objects. Nesting is supported up to two levels deep.
| Field | Type | Description | Example value |
| :---- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
| `and` | `array` | An array of [filter](#type-specific-filter-conditions) objects or compound filter conditions.
Returns data source entries that match **all** of the provided filter conditions. | Refer to the examples below. |
| or | array | An array of [filter](#type-specific-filter-conditions) objects or compound filter conditions.
Returns data source entries that match **any** of the provided filter conditions | Refer to the examples below. |
### Example compound filter conditions
```json Example compound filter condition for a checkbox and number property value expandable theme={null}
{
"filter": {
"and": [
{
"property": "Complete",
"checkbox": {
"equals": true
}
},
{
"property": "Working days",
"number": {
"greater_than": 10
}
}
]
}
}
```
```json Example nested filter condition expandable theme={null}
{
"filter": {
"or": [
{
"property": "Description",
"rich_text": {
"contains": "2023"
}
},
{
"and": [
{
"property": "Department",
"select": {
"equals": "Engineering"
}
},
{
"property": "Priority goal",
"checkbox": {
"equals": true
}
}
]
}
]
}
}
```
# Retrieve block children
Source: https://developers.notion.com/reference/get-block-children
get /v1/blocks/{block_id}/children
Returns a paginated array of child [block objects](/reference/block) contained in the block using the ID specified. In order to receive a complete representation of a block, you may need to recursively retrieve the block children of child blocks.
Page content is represented by block children. See the [Working with page content guide](/guides/data-apis/working-with-page-content#modeling-content-as-blocks) for more information.
Returns only the first level of children for the specified block. See [block objects](/reference/block) for more detail on determining if that block has nested children.
The response may contain fewer than `page_size` of results.
See [Pagination](/reference/intro#pagination) for details about how to use a cursor to iterate through the list.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. Attempting to call this API without read content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the block specified by `id` doesn't exist, or if the connection doesn't have access to the block.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Get databases
Source: https://developers.notion.com/reference/get-databases
get /v1/databases
**Deprecated as of version 2025-09-03**
This endpoint is deprecated and is only available on API version "2021-08-16" and earlier. Use the [Search API](/reference/post-search) instead. This endpoint will only return explicitly shared databases, while search will also return child pages. This endpoint's results cannot be filtered, while search can be used to match on page title.
List all [Databases](/reference/database) shared with the authenticated connection. The response may contain fewer than `page_size` of results.
**Database access**
Connections can only access databases a user has shared with the connection.
See [Pagination](/reference/pagination) for details about how to use a cursor to iterate through the list.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. Attempting to call this API without read content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Retrieve your token's bot user
Source: https://developers.notion.com/reference/get-self
get /v1/users/me
Retrieves the [User](/reference/user) associated with the API token provided in the authorization header.
For [personal access tokens](/guides/get-started/personal-access-tokens), this endpoint returns the user who created the token.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**Connection capabilities**
This endpoint is accessible from by connections with any level of capabilities. The [user object](/reference/user) returned will adhere to the limitations of the connection's capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
# Retrieve a user
Source: https://developers.notion.com/reference/get-user
get /v1/users/{user_id}
Retrieves a [User](/reference/user) using the ID specified.
The requested user must belong to the workspace connected to the integration. This endpoint can return workspace members, guests, and bots in that workspace.
## Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**Connection capabilities**
This endpoint requires a connection to have user information capabilities. Attempting to call this API without user information capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
# List all users
Source: https://developers.notion.com/reference/get-users
get /v1/users
Returns a paginated list of [Users](/reference/user) for the workspace. The response may contain fewer than `page_size` of results.
Guests are not included in the response.
If you already know a guest's ID, [Retrieve a user](/reference/get-user) can return that guest when they belong to the connected workspace.
See [Pagination](/reference/intro#pagination) for details about how to use a cursor to iterate through the list.
The API does not guarantee a particular sort order for returned users.
[Personal access tokens](/guides/get-started/personal-access-tokens) cannot list workspace users. Use [Retrieve token's bot user](/reference/get-self) to retrieve the PAT creator, or [Retrieve a user](/reference/get-user) with that user's ID.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
The API does not currently support filtering users by their email and/or name.
**Connection capabilities**
This endpoint requires a connection to have user information capabilities. Attempting to call this API without user information capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
# Get view query results
Source: https://developers.notion.com/reference/get-view-query-results
get /v1/views/{view_id}/queries/{query_id}
Paginate through cached view query results.
Returns a page of results from a previously [created view query](/reference/create-view-query). Use `start_cursor` and `page_size` to paginate through the cached result set.
Cached results expire after 15 minutes from the time the query was created. If the cache has expired, this endpoint returns a 404.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Truncated queries
If the underlying [view query](/reference/create-view-query) matched more rows than the server-side pagination limit, the response will include a `request_status` field:
```json theme={null}
{
"object": "list",
"type": "page",
"results": [...],
"next_cursor": null,
"has_more": false,
"request_status": {
"type": "incomplete",
"incomplete_reason": "query_result_limit_reached"
}
}
```
The `request_status` field is surfaced on every page of paginated results for a truncated query, so your connection can detect it regardless of which page it is on. When this field is present, there are additional rows matching the view's configuration that are not returned.
See [Create a view query](/reference/create-view-query) for guidance on working around the pagination limit (narrower view filters, [connection webhooks](/reference/webhooks)).
### Errors
Returns a 404 HTTP response if the query doesn't exist, has expired, or the `view_id` doesn't match.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Introduction
Source: https://developers.notion.com/reference/intro
Learn the conventions, authentication, and pagination patterns used across the Notion API.
Connections use the API to access Notion's pages, databases, and users. Connections can connect services to Notion and build interactive experiences for users within Notion. Use the navigation on the left to find details for objects and endpoints used in the API.
A bearer token is required to interact with the Notion API. Use an installation access token from an internal connection, an OAuth access token from a public connection, or a [personal access token](/guides/get-started/personal-access-tokens). If this is your first look at the Notion API, begin with the [Getting started guide](/guides/get-started/overview) to learn which connection type to use.
To work on a specific internal connection without access to its token, confirm that you are an admin in the workspace where the connection was created. Check inside the Notion UI via `Settings & Members` in the left sidebar. If you're not an admin in any of your workspaces, create a personal workspace for free or use a PAT where workspace policy allows it.
## Conventions
The base URL to send all API requests is `https://api.notion.com`. HTTPS is required for all API requests.
The Notion API follows RESTful conventions when possible, with most operations performed via `GET`, `POST`, `PATCH`, and `DELETE` requests on page and database resources. Request and response bodies are encoded as JSON.
### JSON conventions
* Top-level resources have an `"object"` property. This property can be used to determine the type of the resource (e.g. `"database"`, `"user"`, etc.)
* Top-level resources are addressable by a UUIDv4 `"id"` property. You may omit dashes from the ID when making requests to the API, e.g. when copying the ID from a Notion URL.
* Property names are in `snake_case` (not `camelCase` or `kebab-case`).
* Temporal values (dates and datetimes) are encoded in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) strings. Datetimes will include the time value (`2020-08-12T02:12:33.231Z`) while dates will include only the date (`2020-08-12`)
* The Notion API does not support empty strings. To unset a string value for properties like a `url` [page property value](/reference/page-property-values), for example, use an explicit `null` instead of `""`.
## Code samples & SDKs
Samples requests and responses are shown for each endpoint. Requests are shown using the Notion [JavaScript SDK](https://github.com/makenotion/notion-sdk-js), and [cURL](https://curl.se/). These samples make it easy to copy, paste, and modify as you build your connection.
Notion SDKs are open source projects that you can install to easily start building. You may also choose any other language or library that allows you to make HTTP requests.
## Pagination
Endpoints that return lists of objects support cursor-based pagination requests. By default, Notion returns ten items per API call. If the number of items in a response from a support endpoint exceeds the default, then a connection can use pagination to request a specific set of the results and/or to limit the number of returned items.
### Supported endpoints
| HTTP method | Endpoint |
| :---------- | :------------------------------------------------------------------- |
| GET | [List all users](/reference/get-users) |
| GET | [List block children](/reference/get-block-children) |
| GET | [List comments](/reference/list-comments) |
| GET | [Retrieve a page property item](/reference/retrieve-a-page-property) |
| GET | [List file uploads](/reference/list-file-uploads) |
| GET | [List data source templates](/reference/list-data-source-templates) |
| GET | [List views](/reference/list-views) |
| GET | [Get view query results](/reference/get-view-query-results) |
| POST | [Query a data source](/reference/query-a-data-source) |
| POST | [Create a view query](/reference/create-view-query) |
| POST | [Search](/reference/post-search) |
### Responses
If an endpoint supports pagination, then the response object contains the below fields.
| Field | Type | Description |
| :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `has_more` | `boolean` | Whether the response includes the end of the list. `false` if there are no more results. Otherwise, `true`. |
| `next_cursor` | `string` | A string that can be used to retrieve the next page of results by passing the value as the `start_cursor` [parameter](#parameters-for-paginated-requests) to the same endpoint.
Only available when `has_more` is true. |
| `object` | `"list"` | The constant string `"list"`. |
| `results` | `array of objects` | The list, or partial list, of endpoint-specific results. Refer to a [supported endpoint](#supported-endpoints)'s individual documentation for details. |
| `type` | `"block"`
`"comment"`
`"data_source"`
`"file_upload"`
`"page"`
`"page_or_database"`
`"property_item"`
`"template"`
`"user"`
`"view"` | A constant string that represents the type of the objects in `results`. |
| `{type}` | [`paginated list object`](/reference/page-property-values#paginated-page-properties) | An object containing type-specific pagination information. For `property_item`s, the value corresponds to the [paginated page property type](/reference/page-property-values#paginated-page-properties). For all other types, the value is an empty object. |
### Parameters for paginated requests
**Parameter location varies by endpoint**
`GET` requests accept parameters in the query string.
`POST` requests receive parameters in the request body.
| Parameter | Type | Description |
| :------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page_size` | `number` | The number of items from the full list to include in the response.
**Default**: `100` **Maximum**: `100`
The response may contain fewer than the default number of results. |
| `start_cursor` | `string` | A `next_cursor` value returned in a previous [response](#responses). Treat this as an opaque value.
Defaults to `undefined`, which returns results from the beginning of the list. |
### How to send a paginated request
Send an initial request to a [supported endpoint](#supported-endpoints).
Retrieve the `next_cursor` value from the response (only available when `has_more` is `true`).
Send a follow up request to the endpoint that includes the `next_cursor` param in either the query string (for `GET` requests) or in the body params (`POST` requests).
#### Example: paginate through query results from a data source
```bash cURL theme={null}
curl -X POST 'https://api.notion.com/v1/data_sources//query' \
-H 'Authorization: Bearer ' \
-H 'Notion-Version: 2026-03-11' \
-H 'Content-Type: application/json' \
--data '{
"start_cursor": "33e19cb9-751f-4993-b74d-234d67d0d534"
}'
```
```javascript JavaScript SDK theme={null}
import { Client, iteratePaginatedAPI } from "@notionhq/client";
const notion = new Client({
auth: process.env.NOTION_API_KEY,
notionVersion: "2026-03-11",
});
// Option 1: iterate one page of results at a time (async iterator)
for await (const page of iteratePaginatedAPI(
notion.dataSources.query,
{ data_source_id: "" }
)) {
// Process each page result as it arrives
console.log(page);
}
// Option 2: collect all results into an array
import { collectPaginatedAPI } from "@notionhq/client";
const allPages = await collectPaginatedAPI(
notion.dataSources.query,
{ data_source_id: "" }
);
```
# Introspect a token
Source: https://developers.notion.com/reference/introspect-token
post /v1/oauth/introspect
Get a token's active status, scope, and issued time.
# List comments
Source: https://developers.notion.com/reference/list-comments
get /v1/comments
Retrieves a list of un-resolved [Comment objects](/reference/comment-object) from a page or block.
See [Pagination](/reference/intro#pagination) for details about how to use a cursor to iterate through the list.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**Reminder: Turn on connection comment capabilities**
Connection capabilities for reading and inserting comments are off by default.
This endpoint requires a connection to have read comment capabilities. Attempting to call this endpoint without read comment capabilities will return an HTTP response with a 403 status code.
For more information on connection capabilities, see the [capabilities guide](/reference/capabilities). To update your connection settings, visit the Developer portal.
# List custom emojis
Source: https://developers.notion.com/reference/list-custom-emojis
get /v1/custom_emojis
Retrieves a list of custom emojis in the workspace.
See [Pagination](/reference/intro#pagination) for details about how to use a cursor to iterate through the list.
Use the `name` query parameter to filter by exact name match, which is useful for resolving a custom emoji name to its ID.
# List data source templates
Source: https://developers.notion.com/reference/list-data-source-templates
get /v1/data_sources/{data_source_id}/templates
Use this API to retrieve details of all page templates available for a data source.
The response contains a `templates` array with up to 100 results per page. Each template has the following fields:
| Field | Type | Description |
| :----------- | :---------------- | :------------------------------------------------- |
| `id` | `string` (UUIDv4) | The template ID. |
| `name` | `string` | The template's display name. |
| `is_default` | `boolean` | Whether the template is the data source's default. |
### Pagination
If more templates are available, `has_more` is `true` and `next_cursor` contains the cursor for the next page. Pass that value as `start_cursor` in the next request.
### Permissions
This endpoint returns only templates under the specified data source that the connection can access. Sharing the parent database with the connection generally grants access to its child templates.
Templates are also Notion pages. Use [Retrieve a page](/reference/retrieve-a-page) to get a template's properties and content. You can also open a template in Notion and copy its URL to find its ID.
### Errors
Returns a 404 HTTP response if the data source doesn't exist or if the connection doesn't have access to it.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
# List file uploads
Source: https://developers.notion.com/reference/list-file-uploads
get /v1/file_uploads
Use this API to retrieve [file uploads](/reference/file-upload) for the current bot connection, sorted by most recent first.
# List views
Source: https://developers.notion.com/reference/list-views
get /v1/views
List all views in a database.
Returns a paginated list of [View](/reference/view) references for the specified database.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the database doesn't exist, or if the connection doesn't have access.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Move a page
Source: https://developers.notion.com/reference/move-page
post /v1/pages/{page_id}/move
Use this API to move an existing Notion page to a new parent.
## Authentication
Requires [bearer token authentication](/reference/authentication) with appropriate [page edit permissions](/reference/capabilities#content-capabilities).
## Path parameters
**`page_id`** (required)
* **Type**: `string` (UUIDv4)
* **Description**: The ID of the page to move
* This must be a regular Notion page, and not a database. Moving databases or other block types in the API is not currently supported.
* **Format**: UUIDs can be provided with or without dashes
* **Example**: `195de9221179449fab8075a27c979105` or `195de922-1179-449f-ab80-75a27c979105`
## Body parameters
**`parent`** (required)
* **Type**: `object`
* **Description**: The new parent location for the page.
* The bot must have edit access to the new parent.
The `parent` object can be one of two types:
### Page parent
Move the page under another page:
```json JSON theme={null}
{
"parent": {
"type": "page_id",
"page_id": ""
}
}
```
* **`type`**: Always `"page_id"`
* **`page_id`**: UUID of the parent page (with or without dashes)
**Page parent must be a regular Notion page**
The `parent[page_id]` parameter must be a page and cannot be any other type of [block](/reference/block).
One limited exception: for databases that only have a single [data source](/reference/data-source) , the `database_id` *can* be provided under `page_id`, but this is not recommended, since your connection will start encountering HTTP 400 errors if a second data source is added to the database.
### Database parent
Move the page into a database:
```json JSON theme={null}
{
"parent": {
"type": "data_source_id",
"data_source_id": ""
}
}
```
* **`type`**: Always `"data_source_id"`
* **`data_source_id`**: UUID of the database's data source (with or without dashes)
**Note**: You must use `data_source_id` rather than `database_id`. Use the [Retrieve a database](/reference/retrieve-a-database) endpoint to get the child data source ID(s) from the database.
## Example requests
### Move page under another page
```bash cURL theme={null}
curl -X POST https://api.notion.com/v1/pages/195de9221179449fab8075a27c979105/move \
-H "Authorization: Bearer secret_xxx" \
-H "Notion-Version: 2026-03-11" \
-H "Content-Type: application/json" \
-d '{
"parent": {
"type": "page_id",
"page_id": "f336d0bc-b841-465b-8045-024475c079dd"
}
}'
```
### Move page into a database
```bash cURL theme={null}
curl -X POST https://api.notion.com/v1/pages/195de9221179449fab8075a27c979105/move \
-H "Authorization: Bearer secret_xxx" \
-H "Notion-Version: 2026-03-11" \
-H "Content-Type: application/json" \
-d '{
"parent": {
"type": "data_source_id",
"data_source_id": "1c7b35e6-e67f-8096-bf3f-000ba938459e"
}
}'
```
# Page
Source: https://developers.notion.com/reference/page
The Page object contains the [page property values](/reference/page-property-values) of a single Notion page.
```json Example page object expandable theme={null}
{
"object": "page",
"id": "be633bf1-dfa0-436d-b259-571129a590e5",
"created_time": "2022-10-24T22:54:00.000Z",
"last_edited_time": "2023-03-08T18:25:00.000Z",
"created_by": {
"object": "user",
"id": "c2f20311-9e54-4d11-8c79-7398424ae41e"
},
"last_edited_by": {
"object": "user",
"id": "9188c6a5-7381-452f-b3dc-d4865aa89bdf"
},
"cover": null,
"icon": {
"type": "emoji",
"emoji": "🐞"
},
"parent": {
"type": "database_id",
"database_id": "a1d8501e-1ac1-43e9-a6bd-ea9fe6c8822b"
},
"in_trash": true,
"properties": {
"Due date": {
"id": "M%3BBw",
"type": "date",
"date": {
"start": "2023-02-23",
"end": null,
"time_zone": null
}
},
"Status": {
"id": "Z%3ClH",
"type": "status",
"status": {
"id": "86ddb6ec-0627-47f8-800d-b65afd28be13",
"name": "Not started",
"color": "default"
}
},
"Title": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Bug bash",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Bug bash",
"href": null
}
]
}
},
"url": "https://app.notion.com/p/Bug-bash-be633bf1dfa0436db259571129a590e5",
"public_url": "https://jm-testing.notion.site/p1-6df2c07bfc6b4c46815ad205d132e22d"
}
```
All pages have a [Parent](/reference/parent-object). If the parent is a [data source](/reference/data-source), the property values conform to the schema laid out in the data source's [properties](/reference/property-object). Otherwise, the only property value is the `title`.
Page content is available as [blocks](/reference/block). The content can be read using [retrieve block children](/reference/get-block-children) and appended using [append block children](/reference/patch-block-children).
## Page object properties
Properties marked with an \* are available to connections with any capabilities. Other properties require read content capabilities in order to be returned from the Notion API. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
| Property | Type | Description | Example value |
| :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- |
| `object`\* | `string` | Always `"page"`. | `"page"` |
| `id`\* | `string` (UUIDv4) | Unique identifier of the page. | `"45ee8d13-687b-47ce-a5ca-6e2e45548c4b"` |
| `created_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this page was created. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T19:10:04.968Z"` |
| `created_by` | [Partial User](/reference/user) | User who created the page. | `{"object": "user","id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `last_edited_time` | `string` ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | Date and time when this page was updated. Formatted as an [ISO 8601 date time](https://en.wikipedia.org/wiki/ISO_8601) string. | `"2020-03-17T19:10:04.968Z"` |
| `last_edited_by` | [Partial User](/reference/user) | User who last edited the page. | `{"object": "user","id": "45ee8d13-687b-47ce-a5ca-6e2e45548c4b"}` |
| `archived` | `boolean` | **Deprecated.** Use `in_trash` instead. This is an alias for `in_trash` and always returns the same value. | `false` |
| `in_trash` | `boolean` | Whether the page has been trashed. Use this field to check trash status and as a body parameter in [Update page](/reference/patch-page) to trash or restore a page. | `false` |
| `icon` | [Emoji](/reference/emoji-and-icon#emoji), [Icon](/reference/emoji-and-icon#icon), [Custom emoji](/reference/emoji-and-icon#custom-emoji), or [File](/reference/file-object) | Page icon. See [Icon](/reference/page-property-values#icon) for all supported types and read/write variants. | |
| `cover` | [File object](/reference/file-object) (`type` of `"external"` or `"file_upload"` are supported) | Page cover image. | |
| `properties` | `object` | Property values of this page. As of version `2022-06-28`, `properties` only contains the ID of the property; in prior versions `properties` contained the values as well.
If `parent.type` is `"page_id"` or `"workspace"`, then the only valid key is `title`.
If `parent.type` is `"data_source_id"`, then the keys and values of this field are determined by the [`properties`](/reference/property-object) of the [data source](/reference/data-source) this page belongs to.
`key` string Name of a property as it appears in Notion.
`value` object See [Page property values](/reference/page-property-values). | `{ "id": "A%40Hk" }` |
| `parent` | `object` | Information about the page's parent. See [Parent object](/reference/parent-object). | `{ "type": "data_source_id", "data_source_id": "d9824bdc-8445-4327-be8b-5b47500af6ce" }` |
| `url` | `string` | The URL of the Notion page. | `"https://app.notion.com/p/Avocado-d093f1d200464ce78b36e58a3f0d8043"` |
| `public_url` | `string` | The public page URL if the page has been published to the web. Otherwise, `null`. | `"https://jm-testing.notion.site/p1-6df2c07bfc6b4c46815ad205d132e22d"1` |
# Page properties
Source: https://developers.notion.com/reference/page-property-values
## Overview
A [page object](/reference/page) is made up of page properties that contain data about the page.
When you send a request to [Create a page](/reference/post-page), set the page properties in the `properties` object body parameter.
[Retrieve a page](/reference/retrieve-a-page) surfaces the identifier, type, and value of a page’s properties.
[Retrieve a page property item](/reference/retrieve-a-page-property) returns information about a single property ID. Especially for formulas, rollups, and relations, Notion recommends using this API to ensure you get an accurate, up-to-date property value that isn't truncating any results. Refer to [Page property items](/reference/property-item-object) for specific API shape details when using this endpoint.
An [Update page](/reference/patch-page) query modifies the page property values specified in the `properties` object body param.
**Pages that live in a data source are easier to query and manage.**
**Page properties** are most useful when interacting with a page that is an entry in a data source, represented as a row in the Notion app UI.
If a page is not part of a data source, then its only available property is its `title`.
## Attributes
Each page property value object contains the following fields:
| Field | Type | Description | Example value |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------- |
| `id` | `string` | An underlying identifier for the property. Historically, this may be a UUID, but newer IDs are a short ID that's always URL-encoded in the API and in [connection webhooks](/reference/webhooks).
`id` may be used in place of name when creating or updating pages.
`id` remains constant when the property name changes. | `"f%5C%5C%3Ap"` |
| `type` | `string` (enum) | The type of the property in the page object. Possible type values are:
- [`checkbox`](#checkbox) - [`created_by`](#created-by) - [`created_time`](#created-time) - [`date`](#date) - [`email`](#email) - [`files`](#files) - [`formula`](#formula) - [`last_edited_by`](#last-edited-by) - [`last_edited_time`](#last-edited-time) - [`multi_select`](#multi-select) - [`number`](#number) - [`people`](#people) - [`phone_number`](#phone-number) - [`relation`](#relation) - [`rollup`](#rollup) - [`rich_text`](#rich-text) - [`select`](#select) - [`status`](#status) - [`title`](#title) - [`url`](#url) - [`unique_id`](#unique-id) - [`verification`](#verification)Refer to specific type sections below for details on type-specific values. | `"rich_text"` |
| [`checkbox`](#checkbox) [`created_by`](#created-by) [`created_time`](#created-time) [`date`](#date) [`email`](#email) [`files`](#files) [`formula`](#formula) [`last_edited_by`](#last-edited-by) [`last_edited_time`](#last-edited-time) [`multi_select`](#multi-select) [`number`](#number) [`people`](#people) [`phone_number`](#phone-number) [`relation`](#relation) [`rollup`](#rollup) [`rich_text`](#rich-text) [`select`](#select)[`status`](#status) [`title`](#title) [`url`](#url)[`unique_id`](#unique-id) [`verification`](#verification) | `object` | A type object that contains data specific to the page property type, including the page property value.
Refer to the [type objects section](#type-objects) for descriptions and examples of each type. | `"checkbox": true` |
**Size limits for page property values**
For information about size limitations for specific page property objects, refer to the [limits for property values documentation](/reference/request-limits#limits-for-property-values).
When returned from the [Retrieve page property item](/changelog/retrieve-page-property-values) API, there's an additional field, `object`, which is always the string `"property_item"`, as described in [Page property items](/reference/property-item-object).
## Type objects
### Checkbox
Data source property config: [Checkbox](/reference/property-object#checkbox)
| Field | Type | Description | Example value |
| :--------- | :-------- | :--------------------------------------------------------------- | :------------ |
| `checkbox` | `boolean` | Whether the checkbox is checked (`true`) or unchecked (`false`). | `true` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `checkbox` page property value
```json JSON theme={null}
{
"properties": {
"Task completed": {
"checkbox": true
}
}
}
```
#### Example `checkbox` page property value as returned in a GET page request
```json JSON theme={null}
{
"Task completed": {
"id": "ZI%40W",
"type": "checkbox",
"checkbox": true
}
}
```
### Created by
Data source property config: [Created by](/reference/property-object#created-by)
| Field | Type | Description | Example value |
| :----------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------- |
| `created_by` | `object` | A [user object](/reference/user) containing information about the user who created the page.
`created_by` can’t be updated. | Refer to the example response objects below. |
#### Example `created_by` page property value as returned in a GET page request
```json JSON theme={null}
{
"created_by": {
"object": "user",
"id": "c2f20311-9e54-4d11-8c79-7398424ae41e"
}
}
```
### Created time
Data source property config: [Created time](/reference/property-object#created-time)
| Field | Type | Description | Example value |
| :------------- | :-------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :--------------------------- |
| `created_time` | `string` ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time) | The date and time that the page was created.
The `created_time` value can’t be updated. | `"2022-10-12T16:34:00.000Z"` |
#### Example `created_time` page property value as returned in a GET page request
```json JSON theme={null}
{
"Created time": {
"id": "eB_%7D",
"type": "created_time",
"created_time": "2022-10-24T22:54:00.000Z"
}
}
```
### Date
Data source property config: [Date](/reference/property-object#date)
If the `type` of a page property value is `"date"`, then the property value contains a `"date"` object with the following fields:
| Field | Type | Description | Example value |
| :------ | :-------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :----------------------- |
| `end` | `string` ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time) | (Optional) A string representing the end of a date range.
If the value is `null`, then the date value is not a range. | `"2020-12-08T12:00:00Z"` |
| `start` | `string` ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time) | A date, with an optional time.
If the `date` value is a range, then `start` represents the start of the range. | `"2020-12-08T12:00:00Z”` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a date page property value
```json JSON theme={null}
{
"properties": {
"Due date": {
"date": {
"start": "2023-02-23"
}
}
}
}
```
#### Example `date` page property value as returned in a GET page request
```json JSON theme={null}
{
"Due date": {
"id": "M%3BBw",
"type": "date",
"date": {
"start": "2023-02-07",
"end": null,
"time_zone": null
}
}
}
```
### Email
Data source property config: [Email](/reference/property-object#email)
| Field | Type | Description | Example value |
| :------ | :------- | :------------------------------------ | :--------------------- |
| `email` | `string` | A string describing an email address. | `"ada@makenotion.com"` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates an `email` page property value
```json JSON theme={null}
{
"properties": {
"Email": {
"email": "ada@makenotion.com"
}
}
}
```
#### Example `email` page property value as returned in a GET page request
```json json theme={null}
{
"Email": {
"id": "y%5C%5E_",
"type": "email",
"email": "ada@makenotion.com"
}
}
```
### Files
Data source property config: [Files](/reference/property-object#files)
| Field | Type | Description | Example value |
| :------ | :---------------------------------------------- | :---------------------------------------------------------- | :------------------------------------------- |
| `files` | array of [file objects](/reference/file-object) | An array of objects containing information about the files. | Refer to the example response objects below. |
#### Example creation or update of `files` property
The following is an example `properties` body parameter for a `POST` or `PATCH` page request that creates or updates a `files` page property value.
When providing an `external` URL, the `name` parameter is required.
When providing a `file_upload`, the `name` is optional and defaults to the `filename` of the original [File Upload](/reference/file-upload).
```json JSON theme={null}
{
"properties": {
"Blueprint": {
"files": [
{
"name": "Project Alpha blueprint",
"external": {
"url": "https://www.figma.com/file/g7eazMtXnqON4i280CcMhk/project-alpha-blueprint?node-id=0%3A1&t=nXseWIETQIgv31YH-1"
}
}
]
}
}
}
```
#### Example `files` page property value as returned in a GET page request
```json JSON theme={null}
{
"Blueprint": {
"id": "tJPS",
"type": "files",
"files": [
{
"name": "Project blueprint",
"type": "external",
"external": {
"url": "https://www.figma.com/file/g7eazMtXnqON4i280CcMhk/project-alpha-blueprint?node-id=0%3A1&t=nXseWIETQIgv31YH-1"
}
}
]
}
}
```
**Array parameter overwrites the entire existing value**
When updating a `files` page property value, the value is overwritten by the new array of `files` passed.
If you pass a `file` object containing a file hosted by Notion, it remains one of the files. To remove any file, don't pass it in the update request.
### Formula
Data source property config: [Formula](/reference/property-object#formula)
Formula property value objects represent the result of evaluating a formula described in the [data source's properties](/reference/property-object#formula).
If the `type` of a page property value is `"formula"`, then the property value contains a `"formula"` object with the following fields:
| Field | Type | Description | Example value |
| :------------------------------------------------------------------- | :-------------------------------------------------------------- | :------------------------------------------------------------------------ | :------------ |
| `boolean` \|\| `date` \|\| `number` \|\| `string` \|\| `unsupported` | `boolean` \|\| `date` \|\| `number` \|\| `string` \|\| `object` | The formula result. You can't update this value through the API. | 42 |
| `type` | `string` (enum) | The result type: `boolean`, `date`, `number`, `string`, or `unsupported`. | `"number"` |
#### Example `formula` page property value as returned in a GET page request
```json JSON theme={null}
{
"Days until launch": {
"id": "CSoE",
"type": "formula",
"formula": {
"type": "number",
"number": 56
}
}
}
```
The [Retrieve a page endpoint](/reference/retrieve-a-page) returns a maximum of 25 inline page or person references for a `formula` property. If a `formula` property includes more than 25 references, then you can use the [Retrieve a page property item endpoint](/reference/retrieve-a-page-property) for the specific `formula` property to get its complete list of references.
#### Unsupported formula
If the API can't calculate a formula because it depends on too many related pages or nested formulas and rollups, `formula.type` is set to `"unsupported"` and `formula.unsupported` is an empty object. The response doesn't include a partial value. Treat the property as unavailable. To make the value available, reduce the number of related pages or simplify the nested formulas and rollups.
If the page came from a data source query, see [Recommendations for performance](/reference/query-a-data-source#recommendations-for-performance) to request fewer properties and fetch details only for the results you need.
```json Unsupported formula page property value theme={null}
{
"Days until launch": {
"id": "CSoE",
"type": "formula",
"formula": {
"type": "unsupported",
"unsupported": {}
}
}
}
```
### Icon
**Page icon and cover are not nested under `properties`**
The `icon` and `cover` fields in the [Create a page](/reference/post-page) and [Update page](/reference/patch-page) APIs are top-level parameters, not nested under `properties`.
The `icon` field is a discriminated union on the `type` key. Different types are available depending on whether you are reading or writing:
| `type` | Read | Write | Description |
| :--------------- | :--- | :---- | :------------------------------------------------------------------------------------------------------------------------ |
| `"emoji"` | Yes | Yes | A standard emoji character. See [Emoji](/reference/emoji-and-icon#emoji). |
| `"custom_emoji"` | Yes | Yes | A workspace custom emoji, referenced by `id`. See [Custom emoji](/reference/emoji-and-icon#custom-emoji). |
| `"icon"` | Yes | Yes | A native Notion icon with `name` and `color`. See [Icon](/reference/emoji-and-icon#icon). |
| `"external"` | Yes | Yes | An externally hosted image URL. See [File object](/reference/file-object). |
| `"file"` | Yes | No | A Notion-hosted file (uploaded via the UI). Returned in responses only. See [File object](/reference/file-object). |
| `"file_upload"` | No | Yes | A file uploaded via the [File Upload API](/reference/file-upload). Write-only. See [File object](/reference/file-object). |
For full schema details and examples of each type, see the [Emoji and icon](/reference/emoji-and-icon) reference.
#### Example responses
```json Emoji theme={null}
{
"icon": {
"type": "emoji",
"emoji": "😀"
}
}
```
```json Native icon theme={null}
{
"icon": {
"type": "icon",
"icon": {
"name": "pizza",
"color": "blue"
}
}
}
```
```json Custom emoji theme={null}
{
"icon": {
"type": "custom_emoji",
"custom_emoji": {
"id": "45ce454c-d427-4f53-9489-e5d0f3d1db6b",
"name": "bufo",
"url": "https://s3-us-west-2.amazonaws.com/public.notion-static.com/865e85fc-7442-44d3-b323-9b03a2111720/3c6796979c50f4aa.png"
}
}
}
```
```json Notion-hosted file theme={null}
{
"icon": {
"type": "file",
"file": {
"url": "https://local-files-secure.s3.us-west-2.amazonaws.com/...",
"expiry_time": "2024-12-03T19:44:56.932Z"
}
}
}
```
#### Example: setting an icon
```json Emoji theme={null}
{
"icon": { "type": "emoji", "emoji": "🥑" }
}
```
```json Native icon theme={null}
{
"icon": {
"type": "icon",
"icon": { "name": "pizza", "color": "blue" }
}
}
```
```json Custom emoji (by ID) theme={null}
{
"icon": {
"type": "custom_emoji",
"custom_emoji": { "id": "45ce454c-d427-4f53-9489-e5d0f3d1db6b" }
}
}
```
```json File upload theme={null}
{
"icon": {
"type": "file_upload",
"file_upload": { "id": "43833259-72ae-404e-8441-b6577f3159b4" }
}
}
```
To set the **cover** image, use the `cover` parameter with the same file types (`external`, `file_upload`) in the [Create a page](/reference/post-page) or [Update page](/reference/patch-page) request body.
### Last edited by
Data source property config: [Last edited by](/reference/property-object#last-edited-by)
| Field | Type | Description | Example value |
| :--------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------- |
| `last_edited_by` | `object` | A [user object](/reference/user) containing information about the user who last updated the page. `last_edited_by` can’t be updated. | Refer to the example response objects below. |
#### Example `last_edited_by` page property value as returned in a GET page request
```json JSON theme={null}
{
"Last edited by column name": {
"id": "uGNN",
"type": "last_edited_by",
"last_edited_by": {
"object": "user",
"id": "9188c6a5-7381-452f-b3dc-d4865aa89bdf",
"name": "Test Connection",
"avatar_url": "https://s3-us-west-2.amazonaws.com/public.notion-static.com/3db373fe-18f6-4a3c-a536-0f061cb9627f/leplane.jpeg",
"type": "bot",
"bot": {}
}
}
}
```
### Last edited time
Data source property config: [Last edited time](/reference/property-object#last-edited-time)
| Field | Type | Description | Example value |
| :----------------- | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | :--------------------------- |
| `last_edited_time` | `string` ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time) | The date and time that the page was last edited. The `last_edited_time` value can’t be updated. | `"2022-10-12T16:34:00.000Z"` |
#### Example `last_edited_time` page property value as returned in a GET page request
```json JSON theme={null}
{
"Last edited time": {
"id": "%3Defk",
"type": "last_edited_time",
"last_edited_time": "2023-02-24T21:06:00.000Z"
}
}
```
### Multi-select
Data source property config: [Multi-select](/reference/property-object#multi-select)
If the `type` of a page property value is `"multi_select"`, then the property value contains a `"multi_select"` array with the following fields:
| Field | Type | Description | Example value |
| :------ | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------- |
| `color` | `string` (enum) | Color of the option. Note: the `color` value can’t be updated via the API.
Possible `"color"` values are:
- `blue` - `brown` - `default`(the default value) - `gray` - `green` - `orange` - `pink` - `purple` - `red` - `yellow` | `"red"` |
| `id` | `string` | The ID of the option.
You can use `id` or `name` to update a multi-select property. | `"b3d773ca-b2c9-47d8-ae98-3c2ce3b2bffb"` |
| `name` | `string` | The name of the option as it appears in Notion.
If the multi-select [data source property](/reference/property-object) does not yet have an option by that name, then the name will be added to the data source schema if the connection also has write access to the parent data source.
Note: Commas (`","`) are not valid for select values. | `"JavaScript"` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `multi_select` page property value
```json JSON theme={null}
{
"properties": {
"Programming language": {
"multi_select": [
{
"name": "TypeScript"
},
{
"name": "Python"
}
]
}
}
}
```
#### Example `multi_select` page property value as returned in a GET page request
```json JSON theme={null}
{
"Programming language": {
"id": "QyRn",
"name": "Programming language",
"type": "multi_select",
"multi_select": [
{
"id": "tC;=",
"name": "TypeScript",
"color": "purple"
},
{
"id": "e4413a91-9f84-4c4a-a13d-5b4b3ef870bb",
"name": "JavaScript",
"color": "red"
},
{
"id": "fc44b090-2166-40c8-8c58-88f2d8085ec0",
"name": "Python",
"color": "gray"
}
]
}
}
```
If you want to add a new option to a multi-select property via the [Update page](/reference/patch-page) or [Update data source](/reference/update-a-data-source) endpoint, then your connection needs write access to the parent database.
### Number
Data source property config: [Number](/reference/property-object#number)
| Field | Type | Description | Example value |
| :------- | :------- | :-------------------------------- | :------------ |
| `number` | `number` | A number representing some value. | `1234` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `number` page property value
```json JSON theme={null}
{
"properties": {
"Number of subscribers": {
"number": 42
}
}
}
```
#### Example `number` page property value as returned in a GET page request
```json JSON theme={null}
{
"Number of subscribers": {
"id": "WPj%5E",
"type": "number",
"number": 42
}
}
```
### People
Data source property config: [People](/reference/property-object#people)
| Field | Type | Description | Example value |
| :------- | :--------------------------------------- | :------------------------ | :------------------------------------------- |
| `people` | array of [user objects](/reference/user) | An array of user objects. | Refer to the example response objects below. |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `people` page property value
```json JSON theme={null}
{
"properties": {
"Stakeholders": {
"people": [{
"object": "user",
"id": "c2f20311-9e54-4d11-8c79-7398424ae41e"
}]
}
}
}
```
#### Example `people` page property value as returned in a GET page request
```json JSON theme={null}
{
"Stakeholders": {
"id": "%7BLUX",
"type": "people",
"people": [
{
"object": "user",
"id": "c2f20311-9e54-4d11-8c79-7398424ae41e",
"name": "Kimberlee Johnson",
"avatar_url": null,
"type": "person",
"person": {
"email": "[email protected]"
}
}
]
}
}
```
**Retrieve individual property items to avoid truncation**
The [Retrieve a page endpoint](/reference/retrieve-a-page) can’t be guaranteed to return more than 25 people per `people` page property. If a `people` page property includes more than 25 people, then you can use the [Retrieve a page property item endpoint](/reference/retrieve-a-page-property) for the specific `people` property to get a complete list of people.
### Phone number
Data source property config: [Phone number](/reference/property-object#phone-number)
| Field | Type | Description | Example value |
| :------------- | :------- | :------------------------------------------------------------------------ | :--------------- |
| `phone_number` | `string` | A string representing a phone number. No phone number format is enforced. | `"415-867-5309"` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `phone_number` page property value
```json JSON theme={null}
{
"properties": {
"Contact phone number": {
"phone_number": "415-202-4776"
}
}
}
```
#### Example `phone_number` page property value as returned in a GET page request
```json JSON theme={null}
{
"Contact phone number": {
"id": "%5DKhQ",
"type": "phone_number",
"phone_number": "415-202-4776"
}
}
```
### Relation
Data source property config: [Relation](/reference/property-object#relation)
| Field | Type | Description | Example value |
| :--------- | :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------- |
| `has_more` | `boolean` | If a `relation` has more than 25 references, then the `has_more` value for the relation in the response object is `true`. If a relation doesn’t exceed the limit, then `has_more` is `false`. | Refer to the example response objects below. |
| `relation` | an array of page references | An array of related page references. A page reference is an object with an `id` key and a string value corresponding to a page ID in another data source. | Refer to the example response objects below. |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `relation` page property value
```json JSON theme={null}
{
"properties": {
"Related tasks": {
"relation": [
{
"id": "dd456007-6c66-4bba-957e-ea501dcda3a6"
},
{
"id": "0c1f7cb2-8090-4f18-924e-d92965055e32"
}
]
}
}
}
```
#### Example `relation` page property value as returned in a GET page request
```json JSON theme={null}
{
"Related tasks": {
"id": "hgMz",
"type": "relation",
"relation": [
{
"id": "dd456007-6c66-4bba-957e-ea501dcda3a6"
},
{
"id": "0c1f7cb2-8090-4f18-924e-d92965055e32"
}
],
"has_more": false
}
}
```
**To update a `relation` property value via the API, share the related parent database with the connection.**
If a `relation` property value is unexpectedly empty, then make sure that you have shared the original source database for the data source that the `relation` points to with the connection.
Ensuring correct permissions is also important for complete results for `rollup` and `formula` properties.
### Rollup
Data source property config: [Rollup](/reference/property-object#rollup)
If the `type` of a page property value is `"rollup"`, then the property value contains a `"rollup"` object with the following fields:
| Field | Type | Description | Example value |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `array` \|\| `date` \|\| `incomplete` \|\| `number` \|\| `unsupported` | Corresponds to the field.
For example, if the field is `number`, then the type of the value is `number`. | The value of the calculated rollup. The value can't be directly updated via the API. | `1234` |
| `function` | `string` (enum) | The function that is evaluated for every page in the relation of the rollup. Possible `"function"` values are:
- `average` - `checked` - `count` - `count_per_group` - `count_values` - `date_range` - `earliest_date` - `empty` - `latest_date` - `max` - `median` - `min` - `not_empty` - `percent_checked` - `percent_empty` - `percent_not_empty` - `percent_per_group` - `percent_unchecked` - `range` - `show_original` - `show_unique` - `sum` - `unchecked` - `unique` | `"sum"` |
| `type` | `array` \|\| `date` \|\| `incomplete` \|\| `number` \|\| `unsupported` | The value type of the calculated rollup. | `number` |
#### Example `rollup` page property value as returned in a GET page request
```json JSON theme={null}
{
"Number of units": {
"id": "hgMz",
"type": "rollup",
"rollup": {
"type": "number",
"number": 2,
"function": "count"
}
}
}
```
#### Unsupported rollup
If the API can't calculate a rollup because it depends on too many related pages or nested formulas and rollups, `rollup.type` is set to `"unsupported"` and `rollup.unsupported` is an empty object. The response includes the `function` field, but it doesn't include a partial value. Treat the property as unavailable. To make the value available, reduce the number of related pages or simplify the nested formulas and rollups.
If the page came from a data source query, see [Recommendations for performance](/reference/query-a-data-source#recommendations-for-performance) to request fewer properties and fetch details only for the results you need.
```json Unsupported rollup page property value theme={null}
{
"Number of units": {
"id": "hgMz",
"type": "rollup",
"rollup": {
"type": "unsupported",
"unsupported": {},
"function": "count"
}
}
}
```
**For rollup properties with more than 25 references, use the Retrieve a page property endpoint**
Both the [Retrieve a page](/reference/retrieve-a-page) and [Retrieve a page property](/reference/retrieve-a-page-property) endpoints will return information related to the page properties. In cases where a rollup property has more than 25 references, the [Retrieve a page property](/reference/retrieve-a-page-property) endpoint must but used.
Learn more about rollup properties in Notion’s [Help Center](/reference/page-property-values#rollup).
**The API does not support updating `rollup` page property values.**
To change a page's `rollup` property, use the Notion UI.
### Rich text
Data source property config: [Rich text](/reference/property-object#rich-text)
| Field | Type | Description | Example value |
| :---------- | :---------------------------------------------------- | :---------------------------------------------------- | :------------------------------------------- |
| `rich_text` | an array of [rich text objects](/reference/rich-text) | An array of [rich text objects](/reference/rich-text) | Refer to the example response objects below. |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `rich_text` page property value
```json JSON expandable theme={null}
{
"properties": {
"Description": {
"rich_text": [
{
"type": "text",
"text": {
"content": "There is some ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "There is some ",
"href": null
},
{
"type": "text",
"text": {
"content": "text",
"link": null
},
"annotations": {
"bold": true,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "text",
"href": null
},
{
"type": "text",
"text": {
"content": " in this property!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": " in this property!",
"href": null
}
]
}
}
}
```
#### Example `rich_text` page property value as returned in a GET page request
```json JSON expandable theme={null}
{
"Description": {
"id": "HbZT",
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": {
"content": "There is some ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "There is some ",
"href": null
},
{
"type": "text",
"text": {
"content": "text",
"link": null
},
"annotations": {
"bold": true,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "text",
"href": null
},
{
"type": "text",
"text": {
"content": " in this property!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": " in this property!",
"href": null
}
]
}
}
```
The [Retrieve a page endpoint](/reference/retrieve-a-page) returns a maximum of 25 populated inline page or person references for a `rich_text` property. If a `rich_text` property includes more than 25 references, then you can use the [Retrieve a page property item endpoint](/reference/retrieve-a-page-property) for the specific `rich_text` property to get its complete list of references.
### Select
Data source property config: [Select](/reference/property-object#select)
If the type of a page property value is `select`, then the property value contains a `select` object with the following fields:
| Property | Type | Description | Example value |
| :------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------- |
| `color` | `string` (enum) | The color of the option. Possible `"color"` values are:
Defaults to `default`. The `color` value can’t be updated via the API. | `red` |
| `id` | `string` | The ID of the option.
You can use `id` or `name` to [update](/reference/patch-page) a select property. | `"b3d73ca-b2c9-47d8-ae98-3c2ce3b2bffb"` |
| `name` | `string` | The name of the option as it appears in Notion.
If the select [data source property](/reference/property-object) doesn't have an option by that name yet, then the name is added to the data source schema if the connection also has write access to the parent data source.
Note: Commas (`","`) are not valid for select values. | `"jQuery"` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `select` page property value
```json JSON theme={null}
{
"properties": {
"Department": {
"select": {
"name": "Marketing"
}
}
}
}
```
#### Example select page property value as returned in a GET page request
```json JSON theme={null}
{
"Department": {
"id": "Yc%3FJ",
"type": "select",
"select": {
"id": "ou@_",
"name": "jQuery",
"color": "purple"
}
}
}
```
### Status
Data source property config: [Status](/reference/property-object#status)
If the type of a page property value is `status`, then the property value contains a `status` object with the following fields:
| Property | Type | Description | Example value |
| :------- | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `color` | `string` (enum) | The color of the option. Possible `"color"` values are:
- `blue` - `brown` - `default` - `gray` - `green` - `orange` - `pink` - `purple` - `red` - `yellow` Defaults to `default`. The `color` value can’t be updated via the API. | `"red"` |
| `id` | `string` | `string` | `"b3d773ca-b2c9-47d8-ae98-3c2ce3b2bffb"` |
| `name` | `string` | The name of the option as it appears in Notion. | `"In progress"` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `status` page property value
```json JSON theme={null}
{
"properties": {
"Status": {
"status": {
"name": "Not started"
}
}
}
}
```
#### Example `status` page property value as returned in a GET page request
```json JSON theme={null}
{
"Status": {
"id": "Z%3ClH",
"type": "status",
"status": {
"id": "539f2705-6529-42d8-a215-61a7183a92c0",
"name": "In progress",
"color": "blue"
}
}
}
```
### Title
Data source property config: [Title](/reference/property-object#title)
| Field | Type | Description | Example value |
| :------ | :---------------------------------------------------- | :----------------------------------------------------- | :------------------------------------------- |
| `title` | an array of [rich text objects](/reference/rich-text) | An array of [rich text objects](/reference/rich-text). | Refer to the example response objects below. |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `title` page property value
```json JSON expandable theme={null}
{
"properties": {
"Title": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "A better title for the page",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "This is also not done",
"href": null
}
]
}
}
}
```
#### Example `title` page property value as returned in a GET page request
```json JSON expandable theme={null}
{
"Title": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "A better title for the page",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "This is also not done",
"href": null
}
]
}
}
```
The [Retrieve a page endpoint](/reference/retrieve-a-page) returns a maximum of 25 inline page or person references for a `title` property. If a `title` property includes more than 25 references, then you can use the [Retrieve a page property item endpoint](/reference/retrieve-a-page-property) for the specific `title` property to get its complete list of references.
### URL
Data source property config: [URL](/reference/property-object#url)
| Field | Type | Description | Example value |
| :---- | :------- | :------------------------------------- | :--------------------------------- |
| `url` | `string` | A string that describes a web address. | `"https://developers.notion.com/"` |
#### Example `properties` body param for a POST or PATCH page request that creates or updates a `url` page property value
```json JSON theme={null}
{
"properties": {
"Website": {
"url": "https://developers.notion.com/"
}
}
}
```
#### Example `url` page property value as returned in a GET page request
```json JSON theme={null}
{
"Website": {
"id": "bB%3D%5B",
"type": "url",
"url": "https://developers.notion.com/"
}
}
```
### Unique ID
Data source property config: [Unique ID](/reference/property-object#unique-id)
| Field | Type | Description | Example value |
| :------- | :----------------- | :------------------------------------------------- | :------------ |
| `number` | `number` | The ID count (auto-incrementing). | 3 |
| `prefix` | `string` or `null` | An optional prefix to be applied to the unique ID. | "RL" |
Unique IDs can be read using the API with a [GET page](/reference/retrieve-a-page) request, but they cannot be updated with the API, since they are auto-incrementing.
#### Example `unique_id` page property value as returned in a GET page request
```json JSON theme={null}
{
"test-ID": {
"id": "tqqd",
"type": "unique_id",
"unique_id": {
"number": 3,
"prefix": "RL",
},
},
}
```
### Verification
The verification status of a page in a wiki database. Pages can be verified, unverified, or expired (when a verification's end date is in the past).
You can set or update the verification status via the [Create page](/reference/post-page) and [Update page](/reference/patch-page) endpoints. The `verified_by` field is read-only and is automatically set to the acting connection.
The `verification` property is only available for pages that are part of a [wiki database](/guides/data-apis/working-with-databases#wiki-databases). To learn more about wiki databases and verifying pages, see our [Help Center article](https://www.notion.com/help/wikis-and-verified-pages#verifying-pages).
| Field | Type | Description | Example value |
| :------------ | :--------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------- |
| `state` | `string` | The verification state of the page. Possible values: `"verified"`, `"unverified"`, or `"expired"` (returned when the end date is in the past). | `"unverified"` |
| `verified_by` | [User](/reference/user) object or `null` | If the page is verified, a [User](/reference/user) object indicating who verified the page. Read-only — automatically set to the acting connection when writing. | Refer to the example response objects below. |
| `date` | Object or `null` | If the page is verified, the date object will include the date the verification started (`start`). If an expiration date is set for the verification, an end date (`end`) will be included. ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time.) | Refer to the example response objects below. |
#### Example `verification` page property values as returned in a GET page request
**Unverified**
```json JSON theme={null}
{
"Verification": {
"id": "fpVq",
"type": "verification",
"verification": {
"state": "unverified",
"verified_by": null,
"date": null
}
}
}
```
**Verified with no expiration date set**
```json JSON theme={null}
{
"Verification": {
"id": "fpVq",
"type": "verification",
"verification": {
"state": "verified",
"verified_by": {
"object": "user",
"id": "01e46064-d5fb-4444-8ecc-ad47d076f804",
"name": "User Name",
"avatar_url": null,
"type": "person",
"person": {}
},
"date": {
"start": "2023-08-01T04:00:00.000Z",
"end": null,
"time_zone": null
}
}
}
}
```
**Verified with 90-day expiration date**
```json JSON theme={null}
{
"Verification": {
"id": "fpVq",
"type": "verification",
"verification": {
"state": "verified",
"verified_by": {
"object": "user",
"id": "01e46064-d5fb-4444-8ecc-ad47d076f804"
},
"date": {
"start": "2023-08-01T04:00:00.000Z",
"end": "2023-10-30T04:00:00.000Z",
"time_zone": null
}
}
}
}
```
#### Example: set verification via [Update page](/reference/patch-page)
```bash cURL theme={null}
curl https://api.notion.com/v1/pages/60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
-X PATCH \
--data '{
"properties": {
"Verification": {
"verification": {
"state": "verified",
"date": {
"start": "2026-03-25T00:00:00.000Z",
"end": "2026-06-25T00:00:00.000Z"
}
}
}
}
}'
```
```json Unverify a page theme={null}
{
"properties": {
"Verification": {
"verification": {
"state": "unverified"
}
}
}
}
```
The `verified_by` field is ignored when writing — the acting connection is always recorded as the verifier. Do not set the verification `Owner` property in the same request; it is managed automatically.
### Unsupported properties
The Public API supports a subset of property types. Unsupported types will be returned with a `null` value. Exclude these unsupported types when you are updating page properties.
```json JSON theme={null}
{
"properties": {
"Place": {
"id": "%60%40Gq",
"type": "place",
"place": null
}
}
}
```
## Paginated page properties
The `title`, `rich_text`, `relation` and `people` page properties are returned as a paginated `list` object of individual `property_item` objects.
An abridged set of the the properties found in the `list` object is below. Refer to the [pagination documentation](/reference/intro#pagination) for additional information.
| Field | Type | Description | Example value |
| :-------------- | :----------------- | :------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------- |
| `object` | `"list"` | Always `"list"`. | `"list"` |
| `type` | `"property_item"` | Always `"property_item"`. | `"property_item"` |
| `results` | `list` | List of `property_item` objects. | `[{"object": "property_item", "id": "vYdV", "type": "relation", "relation": { "id": "535c3fb2-95e6-4b37-a696-036e5eac5cf6"}}... ]` |
| `property_item` | `object` | A `property_item` object that describes the property. | `{"id": "title", "next_url": null, "type": "title", "title": {}}` |
| `next_url` | `string` or `null` | The URL the user can request to get the next page of results. | `"http://api.notion.com/v1/pages/0e5235bf86aa4efb93aa772cce7eab71/properties/vYdV?start_cursor=LYxaUO&page_size=25"` |
# Parent
Source: https://developers.notion.com/reference/parent-object
Learn more about different parent objects that link together a workspace's entities in Notion's API.
[Pages](/reference/page), [databases](/reference/database), [data sources](/reference/data-source), [comments](/reference/comment-object) and [blocks](/reference/block) are either located inside other pages, databases, data sources, and blocks, or are located at the top level of a workspace. This location is known as the "parent". Parent information is represented by a consistent `parent` object throughout the API.
General parenting rules:
* Pages can be parented by other pages, data sources, blocks, agents, or by the whole workspace.
* *Prior to [API version 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03), page parents were databases, not data sources.*
* *Agent parents only show up on agent instruction pages.*
* Blocks can be parented by pages, data sources, blocks, or agents.
* *Agent parents only show up on blocks inside agent instruction pages.*
* Databases can be parented by pages, blocks, or by the whole workspace.
* *For wikis, databases can also have a data source parent.*
* Data sources are parented by databases.
* *Linked or externally synced external data sources may have data source parents, but aren't thoroughly supported in Notion's API.*
**Exceptions apply**
These parenting rules reflect the possible response you may receive when retrieving information about pages, databases, and blocks via Notion’s REST API in the latest APIversion.
If you are creating new pages, databases, or blocks via Notion’s public REST API, the parenting rules may vary. For example, the parent of a database currently must be a page if it is [created](/reference/create-a-database) via the API.
Refer to the API reference documentation for creating [pages](/reference/post-page), [databases](/reference/create-database), [data sources](/reference/create-a-data-source), and [blocks](/reference/patch-block-children) for more information on current parenting rules.
### Database parent
Database parents most commonly show up for [Data source](/reference/data-source) objects.
| Property | Type | Description | Example values |
| :------------ | :---------------- | :----------------------------------------------------------------------- | :--------------------------------------- |
| `type` | `string` | Always `"database_id"`. | `"database_id"` |
| `database_id` | `string` (UUIDv4) | The ID of the [database](/reference/database) that this page belongs to. | `"b8595b75-abd1-4cad-8dfe-f935a8ef57cb"` |
```json Database parent example theme={null}
{
"type": "database_id",
"database_id": "d9824bdc-8445-4327-be8b-5b47500af6ce"
}
```
### Data source parent
Data source parents most commonly show up for [Page](/reference/page) objects.
| Property | Type | Description | Example values |
| :--------------- | :---------------- | :--------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `type` | `string` | Always `"data_source_id"`. | `"data_source_id"` |
| `data_source_id` | `string` (UUIDv4) | The ID of the [data source](/reference/data-source) that this page belongs to. | `"1a44be12-0953-4631-b498-9e5817518db8"` |
| `database_id` | `string` (UUIDv4) | The ID of the [database](/reference/database) that the data source belongs to, provided in the API response for convenience. | `"b8595b75-abd1-4cad-8dfe-f935a8ef57cb"` |
```json Data source parent example theme={null}
{
"type": "data_source_id",
"data_source_id": "1a44be12-0953-4631-b498-9e5817518db8",
"database_id": "d9824bdc-8445-4327-be8b-5b47500af6ce"
}
```
### Page parent
| Property | Type | Description | Example values |
| :-------- | :---------------- | :--------------------------------------------------------------- | :--------------------------------------- |
| `type` | `string` | Always `"page_id"`. | `"page_id"` |
| `page_id` | `string` (UUIDv4) | The ID of the [page](/reference/page) that this page belongs to. | `"59833787-2cf9-4fdf-8782-e53db20768a5"` |
```json Page parent example theme={null}
{
"type": "page_id",
"page_id": "59833787-2cf9-4fdf-8782-e53db20768a5"
}
```
### Workspace parent
A page or database with a workspace parent is a top-level page within a Notion workspace. Team-level pages are also currently represented as having a workspace parent in the API.
The workspace `parent` object contains the following keys:
| Property | Type | Description | Example values |
| :---------- | :-------- | :-------------------- | :------------- |
| `type` | `type` | Always `"workspace"`. | `"workspace"` |
| `workspace` | `boolean` | Always `true`. | `true` |
```json Workspace parent example theme={null}
{
"type": "workspace",
"workspace": true
}
```
### Block parent
A page may have a block parent if it is created inline in a chunk of text, or is located beneath another block like a toggle or bullet block. The `parent` property is an object containing the following keys:
| Property | Type | Description | Example values |
| :--------- | :---------------- | :--------------------------------------------------------------- | :--------------------------------------- |
| `type` | `type` | Always `"block_id"`. | `"block_id"` |
| `block_id` | `string` (UUIDv4) | The ID of the [page](/reference/page) that this page belongs to. | `"ea29285f-7282-4b00-b80c-32bdbab50261"` |
```json Block parent example theme={null}
{
"type": "block_id",
"block_id": "7d50a184-5bbe-4d90-8f29-6bec57ed817b"
}
```
### Agent parent
Agent parents show up on agent instruction pages and the blocks that make them up, where the parent is the agent the instructions belong to. Most pages and blocks do not have agent parents.
| Property | Type | Description | Example values |
| :--------- | :---------------- | :------------------------------------------------------------ | :--------------------------------------- |
| `type` | `type` | Always `"agent_id"`. | `"agent_id"` |
| `agent_id` | `string` (UUIDv4) | The ID of the agent that owns this instruction page or block. | `"7d50a184-5bbe-4d90-8f29-6bec57ed817b"` |
```json Agent parent example theme={null}
{
"type": "agent_id",
"agent_id": "7d50a184-5bbe-4d90-8f29-6bec57ed817b"
}
```
# Append block children
Source: https://developers.notion.com/reference/patch-block-children
patch /v1/blocks/{block_id}/children
Creates and appends new children blocks to the parent `block_id` specified. Blocks can be parented by other blocks, pages, or databases.
Returns a paginated list of newly created first level children [block objects](/reference/block).
Existing blocks cannot be moved using this endpoint. Once a block is appended as a child, it can't be moved elsewhere via the API.
For blocks that allow children, we allow up to **two** levels of nesting in a single request.
There is a limit of **100 block children** that can be appended by a single API request. Arrays of block children longer than 100 will result in an error.
### Controlling insert position
By default, blocks are appended to the end of the parent block's children. Use the `position` parameter to insert blocks at a specific location:
| Position type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------- |
| `{ "type": "end" }` | Insert at the end of the parent's children (default behavior) |
| `{ "type": "start" }` | Insert at the beginning of the parent's children |
| `{ "type": "after_block", "after_block": { "id": "" } }` | Insert after the specified block |
```json Insert at start theme={null}
{
"children": [/* blocks */],
"position": { "type": "start" }
}
```
```json Insert after specific block theme={null}
{
"children": [/* blocks */],
"position": {
"type": "after_block",
"after_block": { "id": "12345678-1234-1234-1234-123456789abc" }
}
}
```
**Deprecated parameter**
The `after` parameter is deprecated. Use the `position` parameter instead, which provides more flexibility including inserting at the start of the children list.
If you're currently using `after`, migrate to `position` with type `after_block`:
* **Before:** `{ "children": [...], "after": "" }`
* **After:** `{ "children": [...], "position": { "type": "after_block", "after_block": { "id": "" } } }`
You cannot specify both `after` and `position` in the same request.
**Connection capabilities**
This endpoint requires a connection to have insert content capabilities. Attempting to call this API without insert content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the block specified by `id` doesn't exist, or if the connection doesn't have access to the block.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. To see a full description of each type of error code, see the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation.*
# Update page
Source: https://developers.notion.com/reference/patch-page
patch /v1/pages/{page_id}
Use this API to modify attributes of a Notion page, such as its properties, icon, or cover.
### Use cases
#### Updating properties
To change the `properties` of a page in a data source, use the `properties` body parameter. This parameter can only be used if the page's parent is a [data source](/reference/data-source), aside from updating the `title` of a page outside of a data source.
The page’s `properties` schema must match the parent [data source's properties](/reference/property-object).
#### Setting the icon, cover, or "in trash" status
This endpoint can be used to update any page `icon` or `cover`, and can be used to [trash](/reference/trash-page) or restore any page.
#### Locking and unlocking a page
Use the `is_locked` boolean parameter to lock or unlock the page from being further edited in the Notion app UI. Note that this setting doesn't affect the ability to update the page using the API.
#### Applying a page template
Use the `template` body parameter object to apply a [template](/guides/data-apis/creating-pages-from-templates) to an existing page. This can either be the parent data source's default template (`type=default`), or a specific template (`type=template_id`).
You can optionally provide `template[timezone]` — an [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string (e.g. `America/New_York`) — to control the timezone used when resolving template variables like `@now` and `@today`. If omitted, the associated user's timezone is used for public connections and personal access tokens, or UTC for internal connections.
After the API request finishes, Notion's systems merge the content and properties from your chosen template into the current page.
For more information, visit our related guide: [Creating pages from templates](/guides/data-apis/creating-pages-from-templates).
#### Erasing content from a page
Use the `erase_content` flag to delete all block children of the current page. **Use caution** with this parameter, since this is a destructive action that **cannot** be reversed using the API.
The main use case is for applying a `template` in scenarios where it makes sense to clear all of the existing page content and replace it with the template page's content, instead of appending the template content to what's already on the page.
#### Adding content to a page
To add content, use the [append block children](/reference/patch-block-children) API instead. The `page_id` can be passed as the `block_id` when adding block children to the page.
### General behavior
Returns the updated [page object](/reference/page).
**Requirements**
Your connection must have [update content capabilities](/reference/capabilities#content-capabilities) on the target page in order to call this endpoint. To update your connection's capabilities, navigate to the Developer portal, select your connection, open the **Configuration** tab, and scroll to the Capabilities section.
Attempting a query without update content capabilities returns an HTTP response with a 403 status code.
**Limitations**
* Updating [rollup property values](/reference/page-property-values#rollup) is not supported.
* A page’s `parent` cannot be changed.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
# Post database query
Source: https://developers.notion.com/reference/post-database-query
post /v1/databases/{database_id}/query
**Deprecated as of version 2025-09-03**
This page describes the API for versions up to and including `2022-06-28`. In the new `2025-09-03` version, the concepts of databases and data sources were split up, as described in [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
Refer to the new APIs instead:
* [Query a data source](/reference/query-a-data-source)
Gets a list of [Pages](/reference/page) and/or [Databases](/reference/database) contained in the database, filtered and ordered according to the filter conditions and sort criteria provided in the request. The response may contain fewer than `page_size` of results. If the response includes a `next_cursor` value, refer to the [pagination reference](/reference/intro#pagination) for details about how to use a cursor to iterate through the list.
[Wiki](https://www.notion.com/help/wikis-and-verified-pages) databases can contain both pages and databases as children.
[**Filters**](/reference/post-database-query-filter) are similar to the [filters provided in the Notion UI](https://www.notion.com/help/views-filters-and-sorts) where the set of filters and filter groups chained by "And" in the UI is equivalent to having each filter in the array of the compound `"and"` filter. Similar a set of filters chained by "Or" in the UI would be represented as filters in the array of the `"or"` compound filter.
Filters operate on database properties and can be combined. If no filter is provided, non-archived pages in the database will be returned with pagination.
```json Filter Object theme={null}
{
"and": [
{
"property": "Done",
"checkbox": {
"equals": true
}
},
{
"or": [
{
"property": "Tags",
"contains": "A"
},
{
"property": "Tags",
"contains": "B"
}
]
}
]
}
```
In addition to chained filters, databases can be queried with single filters.
```json theme={null}
{
"property": "Done",
"checkbox": {
"equals": true
}
}
```
[**Sorts**](/reference/post-database-query-sort) are similar to the [sorts provided in the Notion UI](https://notion.com/notion/Intro-to-databases-fd8cd2d212f74c50954c11086d85997e#0eb303043b1742468e5aff2f3f670505). Sorts operate on database properties or page timestamps and can be combined. The order of the sorts in the request matter, with earlier sorts taking precedence over later ones.
The properties of the database schema returned in the response body can be filtered with the `filter_properties` query parameter.
```bash theme={null}
https://api.notion.com/v1/databases/[database_id]/query?filter_properties=[property_id_1]
```
Multiple filter properties can be provided by chaining the `filter_properties` query param.
```bash theme={null}
https://api.notion.com/v1/databases/[database_id]/query?filter_properties=[property_id_1]&filter_properties=[property_id_2]
```
Property IDs can be determined with the [Retrieve a database](/reference/retrieve-a-database) endpoint.
If you are using the [Notion JavaScript SDK](https://github.com/makenotion/notion-sdk-js), the `filter_properties` endpoint expects an array of property ID strings.
```javascript JavaScript theme={null}
notion.databases.query({
database_id: id,
filter_properties: ["propertyID1", "propertyID2"]
})
```
**Permissions**
Before a connection can query a database, the database must be shared with the connection. Attempting to query a database that has not been shared will return an HTTP response with a 404 status code.
To share a database with a connection, click the ••• menu at the top right of a database page, scroll to `Add connections`, and use the search bar to find and select the connection from the dropdown list.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. Attempting to call this API without read content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
**To display the page titles of related pages rather than just the ID:**
1. Add a rollup property to the database which uses a formula to get the related page's title. This works well if you have access to updating the database's schema.
2. Otherwise, [retrieve the individual related pages](/reference/retrieve-a-page) using each page ID.
**Formula and Rollup Limitation**
* If a formula depends on a page property that is a relation, and that relation has more than 25 references, only 25 will be evaluated as part of the formula.
* Rollups and formulas that depend on multiple layers of relations may not return correct results.
### Errors
Returns a 404 HTTP response if the database doesn't exist, or if the connection doesn't have access to the database.
Returns a 400 or a 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Filter database entries
Source: https://developers.notion.com/reference/post-database-query-filter
**Deprecated as of version 2025-09-03**
This page describes the API for versions up to and including `2022-06-28`. In the new `2025-09-03` version, the concepts of databases and data sources were split up, as described in [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
Refer to the new page instead:
* [Filter data source entries](/reference/filter-data-source-entries)
When you [query a database](/reference/post-database-query), you can send a `filter` object in the body of the request that limits the returned entries based on the specified criteria.
For example, the below query limits the response to entries where the `"Task completed"` `checkbox` property value is `true`:
```curl cURL theme={null}
curl -X POST 'https://api.notion.com/v1/databases/897e5a76ae524b489fdfe71f5945d1af/query' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H 'Notion-Version: 2022-06-28' \
-H "Content-Type: application/json" \
--data '{
"filter": {
"property": "Task completed",
"checkbox": {
"equals": true
}
}
}'
```
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
// replace with your own database ID
const databaseId = 'd9824bdc-8445-4327-be8b-5b47500af6ce';
const filteredRows = async () => {
const response = await notion.databases.query({
database_id: databaseId,
filter: {
property: "Task completed",
checkbox: {
equals: true
}
},
});
return response;
}
```
Filters can be chained with the `and` and `or` keys so that multiple filters are applied at the same time. (See [Query a database](/reference/post-database-query) for additional examples.)
```json JSON theme={null}
{
"and": [
{
"property": "Done",
"checkbox": {
"equals": true
}
},
{
"or": [
{
"property": "Tags",
"contains": "A"
},
{
"property": "Tags",
"contains": "B"
}
]
}
]
}
```
If no filter is provided, non-archived pages in the database will be returned with pagination.
## The filter object
Each `filter` object contains the following fields:
| Field | Type | Description | Example value |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------- |
| `property` | `string` | The name of the property as it appears in the database, or the property ID. | `"Task completed"` |
| `checkbox` `date` `files` `formula` `multi_select` `number` `people` `phone_number` `relation` `rich_text` `select` `status` `timestamp` `verification` `ID` | `object` | The type-specific filter condition for the query. Only types listed in the Field column of this table are supported.
Refer to [type-specific filter conditions](#type-specific-filter-conditions) for details on corresponding object values. | `"checkbox": { "equals": true }` |
```json Example checkbox filter object theme={null}
{
"filter": {
"property": "Task completed",
"checkbox": {
"equals": true
}
}
}
```
The filter object mimics the database [filter option in the Notion UI](https://www.notion.com/help/views-filters-and-sorts).
## Type-specific filter conditions
### Checkbox
| Field | Type | Description | Example value |
| :--------------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `equals` | `boolean` | Whether a `checkbox` property value matches the provided value exactly.
Returns or excludes all database entries with an exact value match. | `false` |
| `does_not_equal` | `boolean` | Whether a `checkbox` property value differs from the provided value.
Returns or excludes all database entries with a difference in values. | `true` |
```json Example checkbox filter condition theme={null}
{
"filter": {
"property": "Task completed",
"checkbox": {
"does_not_equal": true
}
}
}
```
### Date
For the `after`, `before`, `equals, on_or_before`, and `on_or_after` fields, if a date string with a time is provided, then the comparison is done with millisecond precision.
If no timezone is provided, then the timezone defaults to UTC.
A date filter condition can be used to limit `date` property value types and the [timestamp](#timestamp) property types `created_time` and `last_edited_time`.
The condition contains the below fields:
| Field | Type | Description | Example value |
| :------------- | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------- |
| `after` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns database entries where the date property value is after the provided date. | `"2021-05-10"` `"yesterday"` |
| `before` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns database entries where the date property value is before the provided date. | `"2021-05-10"` `"tomorrow"` |
| `equals` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns database entries where the date property value is the provided date. | `"2021-05-10"` `"today"` |
| `is_empty` | `true` | The value to compare the date property value against. Returns database entries where the date property value contains no data. | `true` |
| `is_not_empty` | `true` | The value to compare the date property value against. Returns database entries where the date property value is not empty. | `true` |
| `next_month` | `object` (empty) | A filter that limits the results to database entries where the date property value is within the next month. | `{}` |
| `next_week` | `object` (empty) | A filter that limits the results to database entries where the date property value is within the next week. | `{}` |
| `next_year` | `object` (empty) | A filter that limits the results to database entries where the date property value is within the next year. | `{}` |
| `on_or_after` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns database entries where the date property value is on or after the provided date. | `"2021-05-10"` `"today"` |
| `on_or_before` | `string` ([ISO 8601 date](https://en.wikipedia.org/wiki/ISO_8601) or [relative date](#relative-date-values)) | The value to compare the date property value against.
Returns database entries where the date property value is on or before the provided date. | `"2021-05-10"` `"today"` |
| `past_month` | `object` (empty) | A filter that limits the results to database entries where the `date` property value is within the past month. | `{}` |
| `past_week` | `object` (empty) | A filter that limits the results to database entries where the `date` property value is within the past week. | `{}` |
| `past_year` | `object` (empty) | A filter that limits the results to database entries where the `date` property value is within the past year. | `{}` |
| `this_week` | `object` (empty) | A filter that limits the results to database entries where the `date` property value is this week. | `{}` |
#### Relative date values
The `after`, `before`, `equals`, `on_or_after`, and `on_or_before` fields accept the following relative date strings in addition to ISO 8601 dates. These are resolved at query time:
| Value | Description |
| :--------------------- | :---------------------------------- |
| `"today"` | The current date. |
| `"tomorrow"` | The day after the current date. |
| `"yesterday"` | The day before the current date. |
| `"one_week_ago"` | Seven days before the current date. |
| `"one_week_from_now"` | Seven days after the current date. |
| `"one_month_ago"` | One month before the current date. |
| `"one_month_from_now"` | One month after the current date. |
```json Example date filter condition theme={null}
{
"filter": {
"property": "Due date",
"date": {
"on_or_after": "2023-02-08"
}
}
}
```
```json Example relative date filter condition theme={null}
{
"filter": {
"property": "Due date",
"date": {
"on_or_after": "today"
}
}
}
```
### Files
| Field | Type | Description | Example value |
| :------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `is_empty` | `true` | Whether the files property value does not contain any data.
Returns all database entries with an empty `files` property value. | `true` |
| `is_not_empty` | `true` | Whether the `files` property value contains data.
Returns all entries with a populated `files` property value. | `true` |
```json Example files filter condition theme={null}
{
"filter": {
"property": "Blueprint",
"files": {
"is_not_empty": true
}
}
}
```
### Formula
The primary field of the `formula` filter condition object matches the type of the formula’s result. For example, to filter a formula property that computes a `checkbox`, use a `formula` filter condition object with a `checkbox` field containing a checkbox filter condition as its value.
| Field | Type | Description | Example value |
| :--------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------- |
| `checkbox` | `object` | A [checkbox](#checkbox) filter condition to compare the formula result against.
Returns database entries where the formula result matches the provided condition. | Refer to the [checkbox](#checkbox) filter condition. |
| `date` | `object` | A [date](#date) filter condition to compare the formula result against.
Returns database entries where the formula result matches the provided condition. | Refer to the [date](#date) filter condition. |
| `number` | `object` | A [number](#number) filter condition to compare the formula result against.
Returns database entries where the formula result matches the provided condition. | Refer to the [number](#number) filter condition. |
| `string` | `object` | A [rich text](#rich-text) filter condition to compare the formula result against.
Returns database entries where the formula result matches the provided condition. | Refer to the [rich text](#rich-text) filter condition. |
```json Example formula filter condition theme={null}
{
"filter": {
"property": "One month deadline",
"formula": {
"date":{
"after": "2021-05-10"
}
}
}
}
```
### Multi-select
| Field | Type | Description | Example value |
| :----------------- | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------- |
| `contains` | `string` or `string[]` | The value(s) to compare the multi-select property value against.
Returns database entries where the multi-select value matches any of the provided values. | `"Marketing"` or `["Marketing", "Sales"]` |
| `does_not_contain` | `string` or `string[]` | The value(s) to compare the multi-select property value against.
Returns database entries where the multi-select value does not match any of the provided values. | `"Engineering"` or `["Engineering", "QA"]` |
| `is_empty` | `true` | Whether the multi-select property value is empty.
Returns database entries where the multi-select value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the multi-select property value is not empty.
Returns database entries where the multi-select value does contains data. | `true` |
```json Example multi-select filter condition theme={null}
{
"filter": {
"property": "Programming language",
"multi_select": {
"contains": "TypeScript"
}
}
}
```
### Number
| Field | Type | Description | Example value |
| :------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `does_not_equal` | `number` | The `number` to compare the number property value against.
Returns database entries where the number property value differs from the provided `number`. | `42` |
| `equals` | `number` | The `number` to compare the number property value against.
Returns database entries where the number property value is the same as the provided number. | `42` |
| `greater_than` | `number` | The `number` to compare the number property value against.
Returns database entries where the number property value exceeds the provided `number`. | `42` |
| `greater_than_or_equal_to` | `number` | The `number` to compare the number property value against.
Returns database entries where the number property value is equal to or exceeds the provided `number`. | `42` |
| `is_empty` | `true` | Whether the `number` property value is empty.
Returns database entries where the number property value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the number property value is not empty.
Returns database entries where the number property value contains data. | `true` |
| `less_than` | `number` | The `number` to compare the number property value against.
Returns database entries where the number property value is less than the provided `number`. | `42` |
| `less_than_or_equal_to` | `number` | The `number` to compare the number property value against.
Returns database entries where the number property value is equal to or is less than the provided `number`. | `42` |
```json Example number filter condition theme={null}
{
"filter": {
"property": "Estimated working days",
"number": {
"less_than_or_equal_to": 5
}
}
}
```
### People
You can apply a people filter condition to `people`, `created_by`, and `last_edited_by` database property types.
The people filter condition contains the following fields:
| Field | Type | Description | Example value |
| :----------------- | :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `contains` | `string` (UUIDv4) or `"me"` | The value to compare the people property value against.
Returns database entries where the people property value contains the provided user. Pass a user UUID or `"me"` to match the current user (see note below). | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `does_not_contain` | `string` (UUIDv4) or `"me"` | The value to compare the people property value against. Returns database entries where the people property value does not contain the provided user. Pass a user UUID or `"me"` to match the current user (see note below). | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `is_empty` | `true` | Whether the people property value does not contain any data.
Returns database entries where the people property value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the people property value contains data.
Returns database entries where the people property value is not empty. | `true` |
```json Example people filter condition theme={null}
{
"filter": {
"property": "Last edited by",
"people": {
"contains": "c2f20311-9e54-4d11-8c79-7398424ae41e"
}
}
}
```
```json Example "me" filter condition theme={null}
{
"filter": {
"property": "Assignee",
"people": {
"contains": "me"
}
}
}
```
The `"me"` value resolves to the user associated with the token. For [public connections](/guides/get-started/overview#connection-types), this is the user who completed the OAuth flow. For [personal access tokens](/guides/get-started/personal-access-tokens), this is the user who created the token. For [internal connections](/guides/get-started/overview#connection-types), there is no associated user — `contains: "me"` returns no results and `does_not_contain: "me"` matches all entries.
### Relation
| Field | Type | Description | Example value |
| :----------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `contains` | `string` (UUIDv4) | The value to compare the relation property value against.
Returns database entries where the relation property value contains the provided `string`. | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `does_not_contain` | `string` (UUIDv4) | The value to compare the relation property value against.
Returns entries where the relation property value does not contain the provided `string`. | `"6c574cee-ca68-41c8-86e0-1b9e992689fb"` |
| `is_empty` | `true` | Whether the relation property value does not contain data.
Returns database entries where the relation property value does not contain any data. | `true` |
| `is_not_empty` | `true` | Whether the relation property value contains data.
Returns database entries where the property value is not empty. | `true` |
```json Example relation filter condition theme={null}
{
"filter": {
"property": "✔️ Task List",
"relation": {
"contains": "0c1f7cb280904f18924ed92965055e32"
}
}
}
```
### Rich text
| Field | Type | Description | Example value |
| :----------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------- |
| `contains` | `string` | The `string` to compare the text property value against.
Returns database entries with a text property value that includes the provided `string`. | `"Moved to Q2"` |
| `does_not_contain` | `string` | The `string` to compare the text property value against.
Returns database entries with a text property value that does not include the provided `string`. | `"Moved to Q2"` |
| `does_not_equal` | `string` | The `string` to compare the text property value against.
Returns database entries with a text property value that does not match the provided `string`. | `"Moved to Q2"` |
| `ends_with` | `string` | The `string` to compare the text property value against.
Returns database entries with a text property value that ends with the provided `string`. | `"Q2"` |
| `equals` | `string` | The `string` to compare the text property value against.
Returns database entries with a text property value that matches the provided `string`. | `"Moved to Q2"` |
| `is_empty` | `true` | Whether the text property value does not contain any data.
Returns database entries with a text property value that is empty. | `true` |
| `is_not_empty` | `true` | Whether the text property value contains any data.
Returns database entries with a text property value that contains data. | `true` |
| `starts_with` | `string` | The `string` to compare the text property value against.
Returns database entries with a text property value that starts with the provided `string`. | "Moved" |
```json Example rich text filter condition theme={null}
{
"filter": {
"property": "Description",
"rich_text": {
"contains": "cross-team"
}
}
}
```
### Rollup
A rollup database property can evaluate to an array, date, or number value. The filter condition for the rollup property contains a `rollup` key and a corresponding object value that depends on the computed value type.
#### Filter conditions for `array` rollup values
| Field | Type | Description | Example value |
| :------ | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------- |
| `any` | `object` | The value to compare each rollup property value against. Can be a [filter condition](#type-specific-filter-conditions) for any other type.
Returns database entries where the rollup property value matches the provided criteria. | `"rich_text": { "contains": "Take Fig on a walk" }` |
| `every` | `object` | The value to compare each rollup property value against. Can be a [filter condition](#type-specific-filter-conditions) for any other type.
Returns database entries where every rollup property value matches the provided criteria. | `"rich_text": { "contains": "Take Fig on a walk" }` |
| `none` | `object` | The value to compare each rollup property value against. Can be a [filter condition](#type-specific-filter-conditions) for any other type.
Returns database entries where no rollup property value matches the provided criteria. | `"rich_text": { "contains": "Take Fig on a walk" }` |
```json Example array rollup filter condition theme={null}
{
"filter": {
"property": "Related tasks",
"rollup": {
"any": {
"rich_text": {
"contains": "Migrate database"
}
}
}
}
}
```
#### Filter conditions for `date` rollup values
A rollup value is stored as a `date` only if the "Earliest date", "Latest date", or "Date range" computation is selected for the property in the Notion UI.
| Field | Type | Description | Example value |
| :----- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------- |
| `date` | `object` | A [date](#date) filter condition to compare the rollup value against.
Returns database entries where the rollup value matches the provided condition. | Refer to the [date](#date) filter condition. |
```json Example date rollup filter condition theme={null}
{
"filter": {
"property": "Parent project due date",
"rollup": {
"date": {
"on_or_before": "2023-02-08"
}
}
}
}
```
#### Filter conditions for `number` rollup values
| Field | Type | Description | Example value |
| :------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
| `number` | `object` | A [number](#number) filter condition to compare the rollup value against.
Returns database entries where the rollup value matches the provided condition. | Refer to the [number](#number) filter condition. |
```json Example number rollup filter condition theme={null}
{
"filter": {
"property": "Total estimated working days",
"rollup": {
"number": {
"does_not_equal": 42
}
}
}
}
```
### Select
| Field | Type | Description | Example value |
| :--------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- |
| `equals` | `string` or `string[]` | The value(s) to compare the select property value against.
Returns database entries where the select property value matches any of the provided values. | `"This week"` or `["Low", "Medium"]` |
| `does_not_equal` | `string` or `string[]` | The value(s) to compare the select property value against.
Returns database entries where the select property value does not match any of the provided values. | `"Backlog"` or `["Done", "Archive"]` |
| `is_empty` | `true` | Whether the select property value does not contain data.
Returns database entries where the select property value is empty. | `true` |
| `is_not_empty` | `true` | Whether the select property value contains data.
Returns database entries where the select property value is not empty. | `true` |
```json Example select filter condition theme={null}
{
"filter": {
"property": "Frontend framework",
"select": {
"equals": "React"
}
}
}
```
### Status
| Field | Type | Description | Example value |
| :--------------- | :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| equals | string or string\[] | The value(s) to compare the status property value against.
Returns database entries where the status property value matches any of the provided values. Status group names (e.g. "To-do", "In progress", "Complete") are also accepted. | "This week" or \["To-do", "In progress"] |
| does\_not\_equal | string or string\[] | The value(s) to compare the status property value against.
Returns database entries where the status property value does not match any of the provided values. | "Backlog" or \["Done", "Archive"] |
| is\_empty | true | Whether the status property value does not contain data.
Returns database entries where the status property value is empty. | true |
| is\_not\_empty | true | Whether the status property value contains data.
Returns database entries where the status property value is not empty. | true |
```json Example status filter condition theme={null}
{
"filter": {
"property": "Project status",
"status": {
"equals": "Not started"
}
}
}
```
### Timestamp
Use a timestamp filter condition to filter results based on `created_time` or `last_edited_time` values.
| Field | Type | Description | Example value |
| :------------------------------- | :------------------------------- | :----------------------------------------------------------------------- | :------------------------------------------- |
| timestamp | created\_time last\_edited\_time | A constant string representing the type of timestamp to use as a filter. | "created\_time" |
| created\_time last\_edited\_time | object | A date filter condition used to filter the specified timestamp. | Refer to the [date](#date) filter condition. |
```json Example timestamp filter condition for created_time theme={null}
{
"filter": {
"timestamp": "created_time",
"created_time": {
"on_or_before": "2022-10-13"
}
}
}
```
The `timestamp` filter condition does not require a property name. The API throws an error if you provide one.
### Verification
| Field | Type | Description | Example value |
| :----- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| status | string | The verification status being queried. Valid options are: `verified`, `expired`, `none`
Returns database entries where the current verification status matches the queried status. | "verified" |
```json Example verification filter condition for getting verified pages theme={null}
{
"filter": {
"property": "verification",
"verification": {
"status": "verified"
}
}
}
```
### ID
Use a timestamp filter condition to filter results based on the `unique_id` value.
| Field | Type | Description | Example value |
| :------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `does_not_equal` | `number` | The value to compare the unique\_id property value against.
Returns database entries where the unique\_id property value differs from the provided value. | `42` |
| `equals` | `number` | The value to compare the unique\_id property value against.
Returns database entries where the unique\_id property value is the same as the provided value. | `42` |
| `greater_than` | `number` | The value to compare the unique\_id property value against.
Returns database entries where the unique\_id property value exceeds the provided value. | `42` |
| `greater_than_or_equal_to` | `number` | The value to compare the unique\_id property value against.
Returns database entries where the unique\_id property value is equal to or exceeds the provided value. | `42` |
| `less_than` | `number` | The value to compare the unique\_id property value against.
Returns database entries where the unique\_id property value is less than the provided value. | `42` |
| `less_than_or_equal_to` | `number` | The value to compare the unique\_id property value against.
Returns database entries where the unique\_id property value is equal to or is less than the provided value. | `42` |
```json Example ID filter condition theme={null}
{
"filter": {
"and": [
{
"property": "ID",
"unique_id": {
"greater_than": 1
}
},
{
"property": "ID",
"unique_id": {
"less_than": 3
}
}
]
}
}
```
## Compound filter conditions
You can use a compound filter condition to limit the results of a database query based on multiple conditions. This mimics filter chaining in the Notion UI.
The above filters in the Notion UI are equivalent to the following compound filter condition via the API:
```json JSON theme={null}
{
"and": [
{
"property": "Done",
"checkbox": {
"equals": true
}
},
{
"or": [
{
"property": "Tags",
"contains": "A"
},
{
"property": "Tags",
"contains": "B"
}
]
}
]
}
```
A compound filter condition contains an `and` or `or` key with a value that is an array of filter objects or nested compound filter objects. Nesting is supported up to two levels deep.
| Field | Type | Description | Example value |
| :---- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
| `and` | `array` | An array of [filter](#type-specific-filter-conditions) objects or compound filter conditions.
Returns database entries that match **all** of the provided filter conditions. | Refer to the examples below. |
| or | array | An array of [filter](#type-specific-filter-conditions) objects or compound filter conditions.
Returns database entries that match **any** of the provided filter conditions | Refer to the examples below. |
### Example compound filter conditions
```json Example compound filter condition for a checkbox and number property value theme={null}
{
"filter": {
"and": [
{
"property": "Complete",
"checkbox": {
"equals": true
}
},
{
"property": "Working days",
"number": {
"greater_than": 10
}
}
]
}
}
```
```json Example nested filter condition theme={null}
{
"filter": {
"or": [
{
"property": "Description",
"rich_text": {
"contains": "2023"
}
},
{
"and": [
{
"property": "Department",
"select": {
"equals": "Engineering"
}
},
{
"property": "Priority goal",
"checkbox": {
"equals": true
}
}
]
}
]
}
}
```
# Sort database entries
Source: https://developers.notion.com/reference/post-database-query-sort
**Deprecated as of version 2025-09-03**
This page describes the API for versions up to and including `2022-06-28`. In the new `2025-09-03` version, the concepts of databases and data sources were split up, as described in [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
Refer to the new page instead:
* [Sort data source entries](/reference/sort-data-source-entries)
A sort is a condition used to order the entries returned from a database query.
A [database query](/reference/post-database-query) can be sorted by a property and/or timestamp and in a given direction. For example, a library database can be sorted by the "Name of a book" (i.e. property) and in `ascending` (i.e. direction).
Here is an example of a sort on a database property.
```json Sorting by "Name" property in ascending direction theme={null}
{
"sorts": [
{
"property": "Name",
"direction": "ascending"
}
]
}
```
If you’re using the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js), you can apply this sorting property to your query like so:
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
// replace with your own database ID
const databaseId = 'd9824bdc-8445-4327-be8b-5b47500af6ce';
const sortedRows = async () => {
const response = await notion.databases.query({
database_id: databaseId,
sorts: [
{
property: "Name",
direction: "ascending"
}
],
});
return response;
}
```
Database queries can also be sorted by two or more properties, which is formally called a nested sort. The sort object listed first in the nested sort list takes precedence.
Here is an example of a nested sort.
```json JSON theme={null}
{
"sorts": [
{
"property": "Food group",
"direction": "descending"
},
{
"property": "Name",
"direction": "ascending"
}
]
}
```
In this example, the database query will first be sorted by "Food group" and the set with the same food group is then sorted by "Name".
## Sort object
### Property value sort
This sort orders the database query by a particular property.
The sort object must contain the following properties:
| Property | Type | Description | Example value |
| :---------- | :-------------- | :------------------------------------------------------------------------------- | :-------------- |
| `property` | `string` | The name of the property to sort against. | `"Ingredients"` |
| `direction` | `string` (enum) | The direction to sort. Possible values include `"ascending"` and `"descending"`. | `"descending"` |
### Entry timestamp sort
This sort orders the database query by the timestamp associated with a database entry.
The sort object must contain the following properties:
| Property | Type | Description | Example value |
| :---------- | :-------------- | :------------------------------------------------------------------------------------------------------------ | :------------------- |
| `timestamp` | `string` (enum) | The name of the timestamp to sort against. Possible values include `"created_time"` and `"last_edited_time"`. | `"last_edited_time"` |
| `direction` | `string` (enum) | The direction to sort. Possible values include `"ascending"` and `"descending"`. | `"descending"` |
# Create a page
Source: https://developers.notion.com/reference/post-page
post /v1/pages
Use this API to create a new [page](/reference/page) as a child of an existing page or [data source](/reference/data-source).
### Use cases
#### Choosing a parent
In most cases, provide a `page_id` or `data_source` under the `parent` parameter to create a page under an existing [page](/reference/page), or [data source](/reference/data-source), respectively.
There is a 3rd option, available for [public connections](/guides/get-started/overview#connection-types) and [personal access tokens](/guides/get-started/personal-access-tokens): creating a private page at the workspace level. To do this, omit the `parent` parameter, or provide `parent[workspace]=true`. This can be useful for quickly creating pages that can then be organized manually in the Notion app later, helping you get to your life's work faster.
For internal connections, a page or data source parent is currently required in the API, because there is no one specific Notion user associated with them that could be used as the "owner" of the new private page.
#### Setting up page properties
If the new page is a child of an existing page,`title` is the only valid property in the `properties` body parameter.
If the new page is a child of an existing [data source](/reference/data-source), the keys of the `properties` object body param must match the parent [data source's properties](/reference/property-object).
#### Setting up page content
This endpoint can be used to create a new page with or without content using the `children` option. To add content to a page after creating it, use the [Append block children](/reference/patch-block-children) endpoint.
**Templates**: As an alternative to building up page content manually, the `template` body parameter can be used to specify an existing data source template to be used to populate the content and properties of the new page.
When omitted, the default is `template[type]=none`, which means no template is applied. The other options for `template[type]` are:
* `default`: Apply the data source's default template.
* This is only allowed for pages created under a data source that has a default template configured in the Notion app.
* `template_id`: Provide a specific `template_id` to use as the blueprint for your page.
* The API bot must have access to the template page, and it must be within the same workspace.
* Although any valid page ID can be used as the `template[template_id]`, we recommend only using pages that are configured as actual [database templates](https://www.notion.com/help/database-templates) under the same data source as the parent of your new page to make sure that page properties can get merged in correctly.
When using `default` or `template_id`, you can optionally provide `template[timezone]` — an [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string (e.g. `America/New_York`) — to control the timezone used when resolving template variables like `@now` and `@today`. If omitted, the associated user's timezone is used for public connections and personal access tokens, or UTC for internal connections. An invalid timezone returns a `validation_error`.
When applying a template, the `children` parameter is **not** allowed. The page is returned as blank initially in the API response, and then Notion's systems apply the template asynchronously after the API request finishes. For more information, see our full guide on [creating pages from templates](/guides/data-apis/creating-pages-from-templates).
### General behavior
Returns a new [page object](/reference/page).
When creating a page with the `markdown` body parameter, you can set `allow_async: true` to receive an HTTP `202` response with an `async_task` object instead of waiting for the page creation to finish in the original request. This is useful for high-block markdown requests that may take longer than typical HTTP client timeout budgets.
If `allow_async` is omitted or `false`, this endpoint keeps its existing synchronous response behavior. `allow_async` changes response behavior only; it does not change validation, permissions, or which operation runs. See [Retrieve an async task](/reference/retrieve-async-task) and [Working with markdown content](/guides/data-apis/working-with-markdown-content#running-large-markdown-writes-asynchronously) for polling examples.
**Newlines in markdown content**
When using the `markdown` body parameter, newlines must be encoded as `\n` in the JSON string — for example, `"# Heading\n\nParagraph"`. The interactive API explorer on this page does not support multiline input, so use cURL, an SDK, or any HTTP client that sends properly encoded JSON. When using cURL, wrap the `--data` body in **single quotes** (`'...'`) so that `\n` is preserved for the JSON parser.
**Some page `properties` are not supported via the API**
A request body that includes `rollup`, `created_by`, `created_time`, `last_edited_by`, or `last_edited_time` values in the properties object returns an error. These Notion-generated values cannot be created or updated via the API. If the `parent` contains any of these properties, then the new page’s corresponding values are automatically created.
**Requirements**
Your connection must have [Insert Content capabilities](/reference/capabilities#content-capabilities) on the target parent page or database in order to call this endpoint. To update your connection's capabilities, navigate to the Developer portal, select your connection, open the **Configuration** tab, and scroll to the Capabilities section.
Attempting a query without update content capabilities returns an HTTP response with a 403 status code.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
# Search by title
Source: https://developers.notion.com/reference/post-search
post /v1/search
Searches all parent or child pages and data_sources that have been shared with a connection.
Returns all [pages](/reference/page) or [data\_sources](/reference/data-source) , excluding duplicated linked databases, that have titles that include the `query` param. If no `query` param is provided, then the response contains all pages or data\_sources that have been shared with the connection. The results adhere to any limitations related to an [connection’s capabilities](/reference/capabilities).
To limit the request to pages or data sources, use the `filter` parameter with `property: "object"` and a `value` of `"page"` or `"data_source"`.
To list content in the trash, set `filter.in_trash` to `true`. You can combine `in_trash` with the object filter or use it by itself.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
The Search endpoint supports pagination. To learn more about working with [paginated](/reference/intro#pagination) responses, see the pagination section of the Notion API Introduction.
To search a specific data\_source — not all sources shared with the connection — use the [Query a data\_source](/reference/query-a-data-source) endpoint instead.
# Page property items
Source: https://developers.notion.com/reference/property-item-object
## Overview
A `property_item` object describes the identifier, type, and value of a page property. It's returned from the [Retrieve a page property item](/reference/retrieve-a-page-property) API.
Generally, the details on this page are the same as those in [Page properties](/reference/page-property-values), but with tweaks and additional information specific to the retrieve page property item endpoint, such as [value pagination](#paginated-values) .
## Common fields
Each page property item object contains the following keys. In addition, it will contain a key corresponding with the value of `type`. The value is an object containing type-specific data. The type-specific data are described in the sections below.
| Property | Type | Description | Example value |
| :------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------- |
| `object` | `"property_item"` | Always `"property_item"`. | `"property_item"` |
| `id` | `string` | Underlying identifier for the property. This identifier is guaranteed to remain constant when the property name changes. It may be a UUID, but is often a short random string.
The `id` may be used in place of `name` when creating or updating pages. | `"f%5C%5C%3Ap"` |
| `type` | `string` (enum) | Type of the property. Possible values are `"rich_text"`, `"number"`, `"select"`, `"multi_select"`, `"date"`, `"formula"`, `"relation"`, `"rollup"`, `"title"`, `"people"`, `"files"`, `"checkbox"`, `"url"`, `"email"`, `"phone_number"`, `"created_time"`, `"created_by"`, `"last_edited_time"`, and `"last_edited_by"`. | `"rich_text"` |
## Paginated values
The [`title`, `rich_text`, `relation` and `people`](/reference/retrieve-a-page-property#paginated-properties) property items of are returned as a paginated `list` object of individual `property_item` objects in the results. An abridged set of the the properties found in the `list` object are found below; see the [Pagination](/reference/pagination) documentation for additional information.
| Property | Type | Description | Example value |
| :-------------- | :----------------- | :------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------- |
| `object` | `"list"` | Always `"list"`. | `"list"` |
| `type` | `"property_item"` | Always `"property_item"`. | `"property_item"` |
| `results` | `list` | List of `property_item` objects. | `[{"object": "property_item", "id": "vYdV", "type": "relation", "relation": { "id": "535c3fb2-95e6-4b37-a696-036e5eac5cf6"}}... ]` |
| `property_item` | `object` | A `property_item` object that describes the property. | `{"id": "title", "next_url": null, "type": "title", "title": {}}` |
| `next_url` | `string` or `null` | The URL the user can request to get the next page of results. | `"http://api.notion.com/v1/pages/0e5235bf86aa4efb93aa772cce7eab71/properties/vYdV?start_cursor=LYxaUO&page_size=25"` |
## Title
Title property value objects contain an array of [rich text objects](/reference/rich-text) within the `title` property.
```json Title property value expandable theme={null}
{
"Name": {
"object": "list",
"results": [
{
"object": "property_item",
"id": "title",
"type": "title",
"title": {
"type": "text",
"text": {
"content": "The title",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "The title",
"href": null
}
}
],
"next_cursor": null,
"has_more": false,
"type": "property_item",
"property_item": {
"id": "title",
"next_url": null,
"type": "title",
"title": {}
}
}
}
```
## Rich text
Rich text property value objects contain an array of [rich text objects](/reference/rich-text) within the `rich_text` property.
```json Rich text property value expandable theme={null}
{
"Details": {
"object": "list",
"results": [
{
"object": "property_item",
"id": "NVv%5E",
"type": "rich_text",
"rich_text": {
"type": "text",
"text": {
"content": "Some more text with ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Some more text with ",
"href": null
}
},
{
"object": "property_item",
"id": "NVv%5E",
"type": "rich_text",
"rich_text": {
"type": "text",
"text": {
"content": "fun formatting",
"link": null
},
"annotations": {
"bold": false,
"italic": true,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "fun formatting",
"href": null
}
}
],
"next_cursor": null,
"has_more": false,
"type": "property_item",
"property_item": {
"id": "NVv^",
"next_url": null,
"type": "rich_text",
"rich_text": {}
}
}
}
```
## Number
Number property value objects contain a number within the `number` property.
```json Number property value expandable theme={null}
{
"Quantity": {
"object": "property_item",
"id": "XpXf",
"type": "number",
"number": 1234
}
}
```
## Select
Select property value objects contain the following data within the `select` property:
| Property | Type | Description | Example value |
| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `id` | `string` (UUIDv4) | ID of the option.
When updating a select property, you can use either `name` or `id`. | `"b3d773ca-b2c9-47d8-ae98-3c2ce3b2bffb"` |
| `name` | `string` | Name of the option as it appears in Notion.
If the select [database property](/reference/property-object) does not yet have an option by that name, it will be added to the database schema if the connection also has write access to the parent database.
Note: Commas (",") are not valid for select values. | `"Fruit"` |
| `color` | `string` (enum) | Color of the option. Possible values are: `"default"`, `"gray"`, `"brown"`, `"red"`, `"orange"`, `"yellow"`, `"green"`, `"blue"`, `"purple"`, `"pink"`.
Defaults to `"default"`. Not currently editable. | `"red"` |
```json Select property value theme={null}
{
"Option": {
"object": "property_item",
"id": "%7CtzR",
"type": "select",
"select": {
"id": "64190ec9-e963-47cb-bc37-6a71d6b71206",
"name": "Option 1",
"color": "orange"
}
}
}
```
## Multi-select
Multi-select property value objects contain an array of multi-select option values within the `multi_select` property.
### Option values
| Property | Type | Description | Example value |
| :------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `id` | `string` (UUIDv4) | ID of the option. When updating a multi-select property, you can use either `name` or `id`. | `"b3d773ca-b2c9-47d8-ae98-3c2ce3b2bffb"` |
| `name` | `string` | Name of the option as it appears in Notion.
If the multi-select [database property](/reference/property-object) does not yet have an option by that name, it will be added to the database schema if the connection also has write access to the parent database.
Note: Commas (",") are not valid for select values. | `"Fruit"` |
| `color` | `string` (enum) | Color of the option. Possible values are: `"default"`, `"gray"`, `"brown"`, `"red"`, `"orange"`, `"yellow"`, `"green"`, `"blue"`, `"purple"`, `"pink"`. Defaults to `"default"`.
Not currently editable. | `"red"` |
```json Multi-select property value theme={null}
{
"Tags": {
"object": "property_item",
"id": "z%7D%5C%3C",
"type": "multi_select",
"multi_select": [
{
"id": "91e6959e-7690-4f55-b8dd-d3da9debac45",
"name": "A",
"color": "orange"
},
{
"id": "2f998e2d-7b1c-485b-ba6b-5e6a815ec8f5",
"name": "B",
"color": "purple"
}
]
}
}
```
## Date
Date property value objects contain the following data within the `date` property:
| Property | Type | Description | Example value |
| :---------- | :---------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------- |
| `start` | string ([ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | An ISO 8601 format date, with optional time. | `"2020-12-08T12:00:00Z"` |
| `end` | string (optional, [ISO 8601 date and time](https://en.wikipedia.org/wiki/ISO_8601)) | An ISO 8601 formatted date, with optional time. Represents the end of a date range.
If `null`, this property's date value is not a range. | `"2020-12-08T12:00:00Z"` |
| `time_zone` | string (optional, enum) | Time zone information for `start` and `end`. Possible values are extracted from the [IANA database](https://www.iana.org/time-zones) and they are based on the time zones from [Moment.js](https://momentjs.com/timezone/).
When time zone is provided, `start` and `end` should not have any [UTC offset](https://en.wikipedia.org/wiki/UTC_offset). In addition, when time zone is provided, `start` and `end` cannot be dates without time information.
If `null`, time zone information will be contained in [UTC offset](https://en.wikipedia.org/wiki/UTC_offset)s in `start` and `end`. | `"America/Los_Angeles"` |
```json Date property value expandable theme={null}
{
"Shipment Time": {
"object": "property_item",
"id": "i%3Ahj",
"type": "date",
"date": {
"start": "2021-05-11T11:00:00.000-04:00",
"end": null,
"time_zone": null
}
}
}
```
## Formula
Formula property value objects represent the result of evaluating a formula described in the [database's properties](/reference/property-object). These objects contain a `type` key and a key corresponding with the value of `type`. The value is an object containing type-specific data. The type-specific data are described in the sections below.
| Property | Type | Description |
| :------- | :-------------- | :------------------------------------------------------------------------ |
| `type` | `string` (enum) | The result type: `string`, `number`, `boolean`, `date`, or `unsupported`. |
### String formula
String formula property values contain an optional string within the `string` property.
### Number formula
Number formula property values contain an optional number within the `number` property.
### Boolean formula
Boolean formula property values contain a boolean within the `boolean` property.
### Date formula
Date formula property values contain an optional [date property value](#date) within the `date` property.
```json Formula Property Value theme={null}
{
"Formula": {
"object": "property_item",
"id": "KpQq",
"type": "formula",
"formula": {
"type": "number",
"number": 1234
}
}
}
```
### Unsupported formula
If the API can't calculate a formula because it depends on too many related pages or nested formulas and rollups, `formula.type` is set to `"unsupported"` and `formula.unsupported` is an empty object. The response doesn't include a partial value. Treat the property as unavailable. To make the value available, reduce the number of related pages or simplify the nested formulas and rollups.
If the page came from a data source query, see [Recommendations for performance](/reference/query-a-data-source#recommendations-for-performance) to request fewer properties and fetch details only for the results you need.
```json Unsupported formula property value theme={null}
{
"Formula": {
"object": "property_item",
"id": "KpQq",
"type": "formula",
"formula": {
"type": "unsupported",
"unsupported": {}
}
}
}
```
## Relation
Relation property value objects contain an array of `relation` property items with page references within the `relation` property. A page reference is an object with an `id` property which is a string value (UUIDv4) corresponding to a page ID in another database.
```json Relation property value expandable theme={null}
{
"Project": {
"object": "list",
"results": [
{
"object": "property_item",
"id": "vYdV",
"type": "relation",
"relation": {
"id": "535c3fb2-95e6-4b37-a696-036e5eac5cf6"
}
}
],
"next_cursor": null,
"has_more": true,
"type": "property_item",
"property_item": {
"id": "vYdV",
"next_url": null,
"type": "relation",
"relation": {}
}
}
}
```
## Rollup
Rollup property value objects represent the result of evaluating a rollup described in the [data source's properties](/reference/property-object). The property is returned as a `list` object of type `property_item` with a list of `relation` items used to computed the rollup under `results`.
A `rollup` property item is also returned under the `property_type` key that describes the rollup aggregation and computed result.
In order to avoid timeouts, if the rollup has a with a large number of aggregations or properties the endpoint returns a `next_cursor` value that is used to determinate the aggregation value *so far* for the subset of relations that have been paginated through.
Once `has_more` is `false`, then the final rollup value is returned. See the [Pagination documentation](/reference/pagination) for more information on pagination in the Notion API.
Computing the values of following aggregations are *not* supported. Instead the endpoint returns a list of `property_item` objects for the rollup:
* `show_unique` (Show unique values)
* `unique` (Count unique values)
* `median`(Median)
| Property | Type | Description |
| :--------- | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `string` (enum) | The type of rollup. Possible values are `"number"`, `"date"`, `"array"`, `"unsupported"` and `"incomplete"`. |
| `function` | `string` (enum) | Describes the aggregation used. Possible values include: `count`, `count_values`, `empty`, `not_empty`, `unique`, `show_unique`, `percent_empty`, `percent_not_empty`, `sum`, `average`, `median`, `min`, `max`, `range`, `earliest_date`, `latest_date`, `date_range`, `checked`, `unchecked`, `percent_checked`, `percent_unchecked`, `count_per_group`, `percent_per_group`, `show_original` |
### Number rollup
Number rollup property values contain a number within the `number` property.
### Date rollup
Date rollup property values contain a [date property value](#date) within the `date` property.
### Array rollup
Array rollup property values contain an array of `property_item` objects within the `results` property.
### Incomplete rollup
Rollups with an aggregation with more than one page of aggregated results will return a `rollup` object of type `"incomplete"`. To obtain the final value paginate through the next values in the rollup using the `next_cursor` or `next_url` property.
```json Rollup Property Value expandable theme={null}
{
"Rollup": {
"object": "list",
"results": [
{
"object": "property_item",
"id": "vYdV",
"type": "relation",
"relation": {
"id": "535c3fb2-95e6-4b37-a696-036e5eac5cf6"
}
}...
],
"next_cursor": "1QaTunT5",
"has_more": true,
"type": "property_item",
"property_item": {
"id": "y}~p",
"next_url": "http://api.notion.com/v1/pages/0e5235bf86aa4efb93aa772cce7eab71/properties/y%7D~p?start_cursor=1QaTunT5&page_size=25",
"type": "rollup",
"rollup": {
"function": "sum",
"type": "incomplete",
"incomplete": {}
}
}
}
}
```
### Unsupported rollup
If the API can't calculate a rollup because it depends on too many related pages or nested formulas and rollups, `rollup.type` is set to `"unsupported"` and `rollup.unsupported` is an empty object. The response includes the `function` field, but it doesn't include a partial value. Treat the property as unavailable. To make the value available, reduce the number of related pages or simplify the nested formulas and rollups.
If the page came from a data source query, see [Recommendations for performance](/reference/query-a-data-source#recommendations-for-performance) to request fewer properties and fetch details only for the results you need.
```json Unsupported rollup property value theme={null}
{
"object": "list",
"results": [
{
"object": "property_item",
"id": "vYdV",
"type": "relation",
"relation": {
"id": "535c3fb2-95e6-4b37-a696-036e5eac5cf6"
}
}
],
"next_cursor": null,
"has_more": false,
"type": "property_item",
"property_item": {
"id": "y}~p",
"next_url": null,
"type": "rollup",
"rollup": {
"function": "sum",
"type": "unsupported",
"unsupported": {}
}
}
}
```
## People
People property value objects contain an array of [user objects](/reference/user) within the `people` property.
```json People property value theme={null}
{
"Owners": {
"object": "property_item",
"id": "KpQq",
"type": "people",
"people": [
{
"object": "user",
"id": "285e5768-3fdc-4742-ab9e-125f9050f3b8",
"name": "Example Avo",
"avatar_url": null,
"type": "person",
"person": {
"email": "[email protected]"
}
}
]
}
}
```
## Files
File property value objects contain an array of file references within the `files` property. A file reference is an object with a [File Object](/reference/file-object) and `name` property, with a string value corresponding to a filename of the original file upload (e.g. `"Whole_Earth_Catalog.jpg"`).
```json json theme={null}
{
"Files": {
"object": "property_item",
"id": "KpQq",
"type": "files",
"files": [
{
"type": "external",
"name": "Space Wallpaper",
"external": "https://website.domain/images/space.png"
}
]
}
}
```
## Checkbox
Checkbox property value objects contain a boolean within the `checkbox` property.
```json Checkbox property value theme={null}
{
"Done?": {
"object": "property_item",
"id": "KpQq",
"type": "checkbox",
"checkbox": true
}
}
```
## URL
URL property value objects contain a non-empty string within the `url` property. The string describes a web address (i.e. `"http://worrydream.com/EarlyHistoryOfSmalltalk/"`).
```json URL property value theme={null}
{
"Website": {
"object": "property_item",
"id": "KpQq",
"type": "url",
"url": "https://notion.com/notiondevs"
}
}
```
## Email
Email property value objects contain a string within the `email` property. The string describes an email address (i.e. `"hello@example.org"`).
```json Email property value theme={null}
{
"Shipper's Contact": {
"object": "property_item",
"id": "KpQq",
"type": "email",
"email": "hello@test.com"
}
}
```
## Phone number
Phone number property value objects contain a string within the `phone_number` property. No structure is enforced.
```json Phone number property value theme={null}
{
"Shipper's No.": {
"object": "property_item",
"id": "KpQq",
"type": "phone_number",
"phone_number": "415-000-1111"
}
}
```
## Created time
Created time property value objects contain a string within the `created_time` property. The string contains the date and time when this page was created. It is formatted as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date time string (i.e. `"2020-03-17T19:10:04.968Z"`).
```json Created time property value theme={null}
{
"Created Time": {
"object": "property_item",
"id": "KpQq",
"type": "create_time",
"created_time": "2020-03-17T19:10:04.968Z"
}
}
```
## Created by
Created by property value objects contain a [user object](/reference/user) within the `created_by` property. The user object describes the user who created this page.
```json Created by property value theme={null}
{
"Created By": {
"created_by": {
"object": "user",
"id": "23345d4f-cf71-4a70-89a5-226c95a6eaae",
"name": "Test User",
"type": "person",
"person": {
"email": "avo@example.org"
}
}
}
}
```
```json Created by property value (using ID) theme={null}
{
"dsEa": {
"created_by": {
"object": "user",
"id": "71e95936-2737-4e11-b03d-f174f6f13087"
}
}
}
```
## Last edited time
Last edited time property value objects contain a string within the `last_edited_time` property. The string contains the date and time when this page was last updated. It is formatted as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date time string (i.e. `"2020-03-17T19:10:04.968Z"`).
```json Last edited time property value theme={null}
{
"Last Edited Time": {
"last_edited_time": "2020-03-17T19:10:04.968Z"
}
}
```
```json Last edited time property value (using ID) theme={null}
{
"as0w": {
"last_edited_time": "2020-03-17T19:10:04.968Z"
}
}
```
## Last edited by
Last edited by property value objects contain a [user object](/reference/user) within the `last_edited_by` property. The user object describes the user who last updated this page.
```json Last edited by property value theme={null}
{
"Last Edited By": {
"last_edited_by": {
"object": "user",
"id": "23345d4f-cf71-4a70-89a5-226c95a6eaae",
"name": "Test User",
"type": "person",
"person": {
"email": "avo@example.org"
}
}
}
}
```
```json Last edited by property value (using ID) theme={null}
{
"as12": {
"last_edited_by": {
"object": "user",
"id": "71e95936-2737-4e11-b03d-f174f6f13087"
}
}
}
```
# Data source properties
Source: https://developers.notion.com/reference/property-object
Data source property objects define the schema of a data source and are rendered as columns in the Notion UI.
All [data source objects](/reference/data-source) include a child `properties` object composed of individual data source property objects. These define the data source schema and are rendered as columns in the Notion UI.
**Looking for page property values?**
To set or read values on individual rows (pages), see [Page property values](/reference/page-property-values). The API treats data source rows as pages.
Every data source property object contains the following fields:
| Field | Type | Description | Example value |
| :------------ | :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `id` | `string` | An identifier for the property, usually a short string of random letters and symbols. Some automatically generated property types have special human-readable IDs (e.g. all Title properties have an `id` of `"title"`). | `"fy:{"` |
| `name` | `string` | The name of the property as it appears in Notion. | |
| `description` | `string` | The description of a property as it appears in Notion. | |
| `type` | `string` (enum) | The type that controls the behavior of the property. Possible values are: `"checkbox"`, `"created_by"`, `"created_time"`, `"date"`, `"email"`, `"files"`, `"formula"`, `"last_edited_by"`, `"last_edited_time"`, `"multi_select"`, `"number"`, `"people"`, `"phone_number"`, `"place"`, `"relation"`, `"rich_text"`, `"rollup"`, `"select"`, `"status"`, `"title"`, `"unique_id"`, `"url"` | `"rich_text"` |
Each data source property object also contains a type object. The key of the object is the `type`, and the value is an object containing type-specific configuration. The following sections detail these type-specific objects.
***
## Checkbox
A checkbox property contains checkboxes. The `checkbox` type object is empty; there is no additional configuration.
```json Example checkbox data source property theme={null}
{
"Task complete": {
"id": "BBla",
"name": "Task complete",
"type": "checkbox",
"checkbox": {}
}
}
```
When [setting page property values](/reference/page-property-values#checkbox), pass a boolean:
```json Example checkbox page property value theme={null}
{
"Task complete": {
"checkbox": true
}
}
```
***
## Created by
Contains people mentions of each row's author. The `created_by` type object is empty. This value is read-only.
```json Example created by data source property theme={null}
{
"Created by": {
"id": "%5BJCR",
"name": "Created by",
"type": "created_by",
"created_by": {}
}
}
```
Returns a [user object](/reference/user). See [Created by page property values](/reference/page-property-values#created-by).
```json Example created by page property value theme={null}
{
"Created by": {
"id": "%5BJCR",
"type": "created_by",
"created_by": {
"object": "user",
"id": "c2f20311-9e54-4d11-8c79-7398424ae41e"
}
}
}
```
***
## Created time
Contains timestamps of when each row was created. The `created_time` type object is empty. This value is read-only.
```json Example created time data source property theme={null}
{
"Created time": {
"id": "XcAf",
"name": "Created time",
"type": "created_time",
"created_time": {}
}
}
```
Returns an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date string. See [Created time page property values](/reference/page-property-values#created-time).
```json Example created time page property value theme={null}
{
"Created time": {
"id": "XcAf",
"type": "created_time",
"created_time": "2022-10-24T22:54:00.000Z"
}
}
```
***
## Date
Contains date values. The `date` type object is empty; there is no additional configuration.
```json Example date data source property theme={null}
{
"Task due date": {
"id": "AJP%7D",
"name": "Task due date",
"type": "date",
"date": {}
}
}
```
Pass a date object with `start` and optional `end` and `time_zone`. See [Date page property values](/reference/page-property-values#date).
```json Example date page property value theme={null}
{
"Task due date": {
"date": {
"start": "2023-02-23",
"end": null,
"time_zone": null
}
}
}
```
***
## Email
Contains email address values. The `email` type object is empty.
```json Example email data source property theme={null}
{
"Contact email": {
"id": "oZbC",
"name": "Contact email",
"type": "email",
"email": {}
}
}
```
Pass a string with the email address. See [Email page property values](/reference/page-property-values#email).
```json Example email page property value theme={null}
{
"Contact email": {
"email": "ada@makenotion.com"
}
}
```
***
## Files
Contains files uploaded to Notion or external links. The `files` type object is empty.
```json Example files data source property theme={null}
{
"Product image": {
"id": "pb%3E%5B",
"name": "Product image",
"type": "files",
"files": {}
}
}
```
Pass an array of [file objects](/reference/file-object). See [Files page property values](/reference/page-property-values#files).
```json Example files page property value theme={null}
{
"Product image": {
"files": [
{
"type": "external",
"name": "Space Wallpaper",
"external": {
"url": "https://website.domain/images/space.png"
}
}
]
}
}
```
***
## Formula
Contains values derived from a provided expression. The `formula` type object has:
| Field | Type | Description | Example value |
| :----------- | :------- | :---------------------------------------------------------------------------------------------------------------------- | :---------------------- |
| `expression` | `string` | The formula used to compute values. Refer to the [Notion help center](https://www.notion.com/help/formulas) for syntax. | `"prop(\"Price\") / 2"` |
```json Example formula data source property theme={null}
{
"Updated price": {
"id": "YU%7C%40",
"name": "Updated price",
"type": "formula",
"formula": {
"expression": "prop(\"Price\") / 2"
}
}
}
```
Returns a computed result. The value can't be updated directly. See [Formula page property values](/reference/page-property-values#formula).
```json Example formula page property value theme={null}
{
"Updated price": {
"id": "YU%7C%40",
"type": "formula",
"formula": {
"type": "number",
"number": 56
}
}
}
```
***
## Last edited by
Contains people mentions of the person who last edited each row. The `last_edited_by` type object is empty. This value is read-only.
```json Example last edited by data source property theme={null}
{
"Last edited by": {
"id": "eB_}",
"name": "Last edited by",
"type": "last_edited_by",
"last_edited_by": {}
}
}
```
Returns a [user object](/reference/user). See [Last edited by page property values](/reference/page-property-values#last-edited-by).
```json Example last edited by page property value theme={null}
{
"Last edited by": {
"id": "eB_}",
"type": "last_edited_by",
"last_edited_by": {
"object": "user",
"id": "9188c6a5-7381-452f-b3dc-d4865aa89bdf"
}
}
}
```
***
## Last edited time
Contains timestamps of when each row was last edited. The `last_edited_time` type object is empty. This value is read-only.
```json Example last edited time data source property theme={null}
{
"Last edited time": {
"id": "jGdo",
"name": "Last edited time",
"type": "last_edited_time",
"last_edited_time": {}
}
}
```
Returns an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date string. See [Last edited time page property values](/reference/page-property-values#last-edited-time).
```json Example last edited time page property value theme={null}
{
"Last edited time": {
"id": "jGdo",
"type": "last_edited_time",
"last_edited_time": "2023-02-24T21:06:00.000Z"
}
}
```
***
## Multi-select
Contains values from a range of options. Each row can have one or more options.
The `multi_select` type object includes an `options` array. Each option has:
| Field | Type | Description | Example value |
| :------ | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `color` | `string` (enum) | The color of the option. Possible values: `blue`, `brown`, `default`, `gray`, `green`, `orange`, `pink`, `purple`, `red`, `yellow` | `"blue"` |
| `id` | `string` | An identifier for the option. Does not change if the name is changed. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
| `name` | `string` | The name of the option as it appears in Notion. Commas are not valid. Names must be unique (case-insensitive). | `"Fruit"` |
```json Example multi-select data source property expandable theme={null}
{
"Store availability": {
"id": "flsb",
"name": "Store availability",
"type": "multi_select",
"multi_select": {
"options": [
{
"id": "5de29601-9c24-4b04-8629-0bca891c5120",
"name": "Duc Loi Market",
"color": "blue"
},
{
"id": "385890b8-fe15-421b-b214-b02959b0f8d9",
"name": "Rainbow Grocery",
"color": "gray"
}
]
}
}
}
```
Pass an array of option objects with `name` or `id`. See [Multi-select page property values](/reference/page-property-values#multi-select).
```json Example multi-select page property value theme={null}
{
"Store availability": {
"multi_select": [
{ "name": "Duc Loi Market" },
{ "name": "Rainbow Grocery" }
]
}
}
```
***
## Number
Contains numeric values. The `number` type object has:
| Field | Type | Description | Example value |
| :------- | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `format` | `string` (enum) | How the number displays in Notion. Values include: `number`, `number_with_commas`, `percent`, `dollar`, `euro`, `pound`, `yen`, `yuan`, `won`, `ruble`, `rupee`, `franc`, `real`, `lira`, `krona`, `ringgit`, and [more](/reference/property-schema-object#number-configuration). | `"percent"` |
```json Example number data source property theme={null}
{
"Price": {
"id": "%7B%5D_P",
"name": "Price",
"type": "number",
"number": {
"format": "dollar"
}
}
}
```
Pass a number. See [Number page property values](/reference/page-property-values#number).
```json Example number page property value theme={null}
{
"Price": {
"number": 42
}
}
```
***
## People
Contains people mentions. The `people` type object is empty.
```json Example people data source property theme={null}
{
"Project owner": {
"id": "FlgQ",
"name": "Project owner",
"type": "people",
"people": {}
}
}
```
Pass an array of [user objects](/reference/user) with `id`. See [People page property values](/reference/page-property-values#people).
```json Example people page property value theme={null}
{
"Project owner": {
"people": [
{
"object": "user",
"id": "c2f20311-9e54-4d11-8c79-7398424ae41e"
}
]
}
}
```
***
## Phone number
Contains phone number values. The `phone_number` type object is empty. No format is enforced.
```json Example phone number data source property theme={null}
{
"Contact phone number": {
"id": "ULHa",
"name": "Contact phone number",
"type": "phone_number",
"phone_number": {}
}
}
```
Pass a string. See [Phone number page property values](/reference/page-property-values#phone-number).
```json Example phone number page property value theme={null}
{
"Contact phone number": {
"phone_number": "415-867-5309"
}
}
```
***
## Place
Contains location values. Can be used with the Map view. The `place` type object is empty.
Place page property values are not fully supported via the API. Reading a place property returns `null`. See [Unsupported properties](/reference/page-property-values#unsupported-properties).
```json Example place data source property theme={null}
{
"Place": {
"id": "Xqz4",
"name": "Place",
"type": "place",
"place": {}
}
}
```
Place values are currently not fully supported via the API. See [Unsupported properties](/reference/page-property-values#unsupported-properties).
```json Example place page property value (read-only) theme={null}
{
"Place": {
"id": "%60%40Gq",
"type": "place",
"place": null
}
}
```
***
## Relation
Contains [relations](https://www.notion.com/help/relations-and-rollups) — references to pages in another data source.
The `relation` type object has:
| Field | Type | Description | Example value |
| :--------------- | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `data_source_id` | `string` (UUID) | The data source that the relation refers to. Linked page values must belong to this data source. | `"668d797c-76fa-4934-9b05-ad288df2d136"` |
| `dual_property` | `object` | An object with `synced_property_id` and `synced_property_name` for the corresponding property in the related data source. Present for dual (bidirectional) relations. | See example below. |
```json Example relation data source property theme={null}
{
"Projects": {
"id": "~pex",
"name": "Projects",
"type": "relation",
"relation": {
"data_source_id": "6c4240a9-a3ce-413e-9fd0-8a51a4d0a49b",
"dual_property": {
"synced_property_name": "Tasks",
"synced_property_id": "JU]K"
}
}
}
}
```
Pass an array of page references with `id`. See [Relation page property values](/reference/page-property-values#relation).
```json Example relation page property value theme={null}
{
"Projects": {
"relation": [
{ "id": "dd456007-6c66-4bba-957e-ea501dcda3a6" },
{ "id": "0c1f7cb2-8090-4f18-924e-d92965055e32" }
]
}
}
```
**Related databases must be shared with your connection**
To retrieve or update relation properties, the related database must also be shared with your connection.
***
## Rich text
Contains text values. The `rich_text` type object is empty.
```json Example rich text data source property theme={null}
{
"Project description": {
"id": "NZZ%3B",
"name": "Project description",
"type": "rich_text",
"rich_text": {}
}
}
```
Pass an array of [rich text objects](/reference/rich-text). See [Rich text page property values](/reference/page-property-values#rich-text).
```json Example rich text page property value theme={null}
{
"Project description": {
"rich_text": [
{
"type": "text",
"text": { "content": "A project description" }
}
]
}
}
```
***
## Rollup
Contains values pulled from a related data source via a [rollup](https://www.notion.com/help/relations-and-rollups).
The `rollup` type object has:
| Field | Type | Description | Example value |
| :----------------------- | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------- |
| `function` | `string` (enum) | The function that computes the rollup value. Values include: `average`, `checked`, `count`, `count_values`, `date_range`, `earliest_date`, `empty`, `latest_date`, `max`, `median`, `min`, `not_empty`, `percent_checked`, `percent_empty`, `percent_not_empty`, `percent_unchecked`, `range`, `show_original`, `show_unique`, `sum`, `unchecked`, `unique` | `"sum"` |
| `relation_property_id` | `string` | The `id` of the related data source property. | `"fy:{"` |
| `relation_property_name` | `string` | The `name` of the related data source property. | `"Tasks"` |
| `rollup_property_id` | `string` | The `id` of the property being rolled up. | `"fy:{"` |
| `rollup_property_name` | `string` | The `name` of the property being rolled up. | `"Days to complete"` |
```json Example rollup data source property theme={null}
{
"Estimated total project time": {
"id": "%5E%7Cy%3C",
"name": "Estimated total project time",
"type": "rollup",
"rollup": {
"rollup_property_name": "Days to complete",
"relation_property_name": "Tasks",
"rollup_property_id": "\\nyY",
"relation_property_id": "Y]
Returns a computed result. The value can't be updated directly. See [Rollup page property values](/reference/page-property-values#rollup).
```json Example rollup page property value theme={null}
{
"Estimated total project time": {
"id": "%5E%7Cy%3C",
"type": "rollup",
"rollup": {
"type": "number",
"number": 14,
"function": "sum"
}
}
}
```
***
## Select
Contains values from a selection of options. Only one option per row.
The `select` type object includes an `options` array. Each option has:
| Field | Type | Description | Example value |
| :------ | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `color` | `string` (enum) | The color of the option. Possible values: `blue`, `brown`, `default`, `gray`, `green`, `orange`, `pink`, `purple`, `red`, `yellow` | `"red"` |
| `id` | `string` | An identifier for the option. Does not change if the name is changed. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
| `name` | `string` | The name of the option as it appears in Notion. Commas are not valid. Names must be unique (case-insensitive). | `"Fruit"` |
```json Example select data source property expandable theme={null}
{
"Food group": {
"id": "%40Q%5BM",
"name": "Food group",
"type": "select",
"select": {
"options": [
{
"id": "e28f74fc-83a7-4469-8435-27eb18f9f9de",
"name": "Vegetable",
"color": "purple"
},
{
"id": "6132d771-b283-4cd9-ba44-b1ed30477c7f",
"name": "Fruit",
"color": "red"
},
{
"id": "fc9ea861-820b-4f2b-bc32-44ed9eca873c",
"name": "Protein",
"color": "yellow"
}
]
}
}
}
```
Pass an option object with `name` or `id`. See [Select page property values](/reference/page-property-values#select).
```json Example select page property value theme={null}
{
"Food group": {
"select": {
"name": "Fruit"
}
}
}
```
***
## Status
Contains values from a list of status options, organized into groups.
The `status` type object includes `options` and `groups` arrays.
**Options** — each has:
| Field | Type | Description | Example value |
| :------ | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `color` | `string` (enum) | The color of the option. Possible values: `blue`, `brown`, `default`, `gray`, `green`, `orange`, `pink`, `purple`, `red`, `yellow` | `"green"` |
| `id` | `string` | An identifier for the option. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
| `name` | `string` | The name of the option as it appears in Notion. Commas are not valid. Names must be unique (case-insensitive). | `"In progress"` |
**Groups** — each has:
| Field | Type | Description | Example value |
| :----------- | :--------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `color` | `string` (enum) | The color of the group. Possible values: `blue`, `brown`, `default`, `gray`, `green`, `orange`, `pink`, `purple`, `red`, `yellow` | `"purple"` |
| `id` | `string` | An identifier for the group. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
| `name` | `string` | The name of the group as it appears in Notion. | `"To do"` |
| `option_ids` | array of strings | Sorted list of `id`s of options that belong to this group. | |
```json Example status data source property expandable theme={null}
{
"Status": {
"id": "biOx",
"name": "Status",
"type": "status",
"status": {
"options": [
{ "id": "034ece9a-384d-4d1f-97f7-7f685b29ae9b", "name": "Not started", "color": "default" },
{ "id": "330aeafb-598c-4e1c-bc13-1148aa5963d3", "name": "In progress", "color": "blue" },
{ "id": "497e64fb-01e2-41ef-ae2d-8a87a3bb51da", "name": "Done", "color": "green" }
],
"groups": [
{ "id": "b9d42483-e576-4858-a26f-ed940a5f678f", "name": "To-do", "color": "gray", "option_ids": ["034ece9a-384d-4d1f-97f7-7f685b29ae9b"] },
{ "id": "cf4952eb-1265-46ec-86ab-4bded4fa2e3b", "name": "In progress", "color": "blue", "option_ids": ["330aeafb-598c-4e1c-bc13-1148aa5963d3"] },
{ "id": "4fa7348e-ae74-46d9-9585-e773caca6f40", "name": "Complete", "color": "green", "option_ids": ["497e64fb-01e2-41ef-ae2d-8a87a3bb51da"] }
]
}
}
}
```
Pass an option object with `name` or `id`. See [Status page property values](/reference/page-property-values#status).
```json Example status page property value theme={null}
{
"Status": {
"status": {
"name": "In progress"
}
}
}
```
When creating a status property without specifying options, defaults ("Not started", "In progress", "Done") with groups ("To-do", "In progress", "Complete") are created. When creating or updating custom options, pass `group` on each option to assign it to `To-do`, `In progress`, or `Complete`. If `group` is omitted on update, existing options keep their current group, and new options use `To-do` when present or the first existing group otherwise. To rename, reorder, or otherwise reconfigure groups, use the Notion UI.
***
## Title
Controls the title that appears at the top of a page when a data source row is opened. The `title` type object is empty.
```json Example title data source property theme={null}
{
"Project name": {
"id": "title",
"name": "Project name",
"type": "title",
"title": {}
}
}
```
Pass an array of [rich text objects](/reference/rich-text). See [Title page property values](/reference/page-property-values#title).
```json Example title page property value theme={null}
{
"Project name": {
"title": [
{
"type": "text",
"text": { "content": "My project" }
}
]
}
}
```
**All data sources require exactly one `title` property.**
The API throws errors if you create a data source without a `title` property, or attempt to add or remove a `title` property.
**Title data source property vs. data source title**
A `title` data source property is a type of column in a data source. A data source `title` defines the name of the data source itself, found on the [data source object](/reference/data-source). Every data source requires both.
***
## Unique ID
Automatically incremented, unique across all pages in a data source. Useful for task or bug report IDs (e.g. `TASK-1234`). This value is read-only.
The `unique_id` type object has an optional `prefix`:
| Field | Type | Description | Example value |
| :------- | :----------------- | :-------------------------------------------------------------------------------------------- | :------------ |
| `prefix` | `string` or `null` | A common prefix assigned to pages. When set, enables lookup URLs like `notion.com/TASK-1234`. | `"TASK"` |
```json Example unique ID data source property theme={null}
{
"Task ID": {
"id": "tqqd",
"name": "Task ID",
"type": "unique_id",
"unique_id": {
"prefix": "TASK"
}
}
}
```
Returns the auto-incremented number and optional prefix. See [Unique ID page property values](/reference/page-property-values#unique-id).
```json Example unique ID page property value theme={null}
{
"Task ID": {
"id": "tqqd",
"type": "unique_id",
"unique_id": {
"number": 3,
"prefix": "TASK"
}
}
}
```
***
## URL
Contains URL values. The `url` type object is empty.
```json Example URL data source property theme={null}
{
"Project URL": {
"id": "BZKU",
"name": "Project URL",
"type": "url",
"url": {}
}
}
```
Pass a string with the URL. See [URL page property values](/reference/page-property-values#url).
```json Example URL page property value theme={null}
{
"Project URL": {
"url": "https://developers.notion.com/"
}
}
```
# Query a data source
Source: https://developers.notion.com/reference/query-a-data-source
post /v1/data_sources/{data_source_id}/query
### Overview
Gets a list of [pages](/reference/page) contained in the data source, filtered and ordered according to the filter conditions and sort criteria provided in the request. The response may contain fewer than `page_size` of results. If the response includes a `next_cursor` value, refer to the [pagination reference](/reference/intro#pagination) for details about how to use a cursor to iterate through the list.
**Databases, data sources, and wikis**
[Wiki](https://www.notion.com/help/wikis-and-verified-pages) data sources can contain either pages or databases as children. In all other cases, the children can only be pages.
For wikis, instead of directly returning any [database](/reference/database) results, this API returns all [data sources](/reference/data-source) that are children of *that* database. Surfacing the data source instead of the direct database child helps make it easier to craft your next API request (for example, retrieving the data source or listing its children.)
Another tip for wikis is to use the `result_type` filter of `"page"` or `"data_source"` if you're only looking for query results that are one of those two types instead of both.
### Filtering
[**Filters**](/reference/filter-data-source-entries) are similar to the [filters provided in the Notion UI](https://www.notion.com/help/views-filters-and-sorts) where the set of filters and filter groups chained by "And" in the UI is equivalent to having each filter in the array of the compound `"and"` filter. Similar a set of filters chained by "Or" in the UI would be represented as filters in the array of the `"or"` compound filter.
Filters operate on data source properties and can be combined. If no filter is provided, non-archived pages in the data source are returned with pagination.
```json Filter object expandable theme={null}
{
"and": [
{
"property": "Done",
"checkbox": {
"equals": true
}
},
{
"or": [
{
"property": "Tags",
"contains": "A"
},
{
"property": "Tags",
"contains": "B"
}
]
}
]
}
```
In addition to chained filters, data sources can be queried with single filters.
```json JSON theme={null}
{
"property": "Done",
"checkbox": {
"equals": true
}
}
```
### Archived pages
By default, this endpoint returns non-archived pages. To query archived pages instead, set the top-level `is_archived` body parameter to `true`:
```json theme={null}
{
"is_archived": true
}
```
Set `is_archived` to `false`, or omit it, to query non-archived pages. The archive selector is applied before property filters and sorts, so filters only match rows in the selected archive partition.
`is_archived` is separate from `in_trash`. Page objects include `is_archived` to indicate whether a page is archived, while `in_trash` indicates trash status and is used by page/block/data source update APIs. `in_trash` is not a supported query body parameter for this endpoint.
### Sorting
[**Sorts**](/reference/sort-data-source-entries) are similar to the [sorts provided in the Notion UI](https://notion.com/notion/Intro-to-databases-fd8cd2d212f74c50954c11086d85997e#0eb303043b1742468e5aff2f3f670505). Sorts operate on database properties or page timestamps and can be combined. The order of the sorts in the request matter, with earlier sorts taking precedence over later ones.
Notion doesn't guarantee any particular sort order when no sort parameters are provided.
### Pagination limit
This endpoint supports paginating through up to **10,000 results** per query. If a data source contains more matching entries than this limit, pagination stops at the 10,000th result: `has_more` becomes `false`, and every response page served from the capped result includes a `request_status` marking the result as incomplete:
```json theme={null}
{
"request_status": {
"type": "incomplete",
"incomplete_reason": "query_result_limit_reached"
}
}
```
Check `request_status.type === "incomplete"` on every response page to know whether a query was cut off. Any page with that status means the whole query result was capped. The limit is per query (a query is defined by its filter and sort), not per data source.
For connections that need to process all pages in a large data source, we recommend:
* Reading every row past the limit by partitioning the query into `created_time` windows. See [Query large data sources](/guides/data-apis/query-large-data-sources) for the technique and a [runnable cookbook example](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/query-large-data-sources).
* Using [filters](/reference/filter-data-source-entries) to narrow the result set (e.g. filter by `last_edited_time` to fetch only recently changed pages).
* Setting up [connection webhooks](/reference/webhooks) for incremental sync instead of polling the full data source on a schedule.
**Incremental sync via webhooks**
If your connection polls this endpoint on a recurring schedule to detect changes, consider switching to [connection webhooks](/reference/webhooks) instead. Webhooks notify your connection of changes in real time, eliminating the need to paginate through the entire data source. This is faster, more efficient, and avoids hitting the pagination limit.
### Recommendations for performance
Use the `filter_properties` query parameter whenever you don't need every property from every result. This endpoint returns only the properties you list. For example:
```bash theme={null}
https://api.notion.com/v1/data_sources/[DATA_SOURCE_ID]/query?filter_properties[]=title
```
Multiple filter properties can be provided by chaining the `filter_properties` query param. For example:
```bash theme={null}
https://api.notion.com/v1/data_sources/[DATA_SOURCE_ID]/query?filter_properties[]=title&filter_properties[]=status
```
This parameter accepts property IDs or property names. Property IDs can be determined with the [Retrieve a data source](/reference/retrieve-a-data-source) endpoint.
If you are using the [Notion JavaScript SDK](https://github.com/makenotion/notion-sdk-js), the `filter_properties` endpoint expects an array of strings. For example:
```typescript TypeScript theme={null}
notion.dataSources.query({
data_source_id: id,
filter_properties: ["title", "status"]
})
```
Using `filter_properties` can speed up the query and reduce its response size, especially for data sources with many properties, formulas, rollups, or relations. Use this endpoint to find pages and return only the properties your list shows. Fetch more data after you select a page:
* Call [Retrieve a page](/reference/retrieve-a-page) with the returned page ID to get more properties from one page. You can also pass `filter_properties` to that endpoint.
* Call [Retrieve a page property](/reference/retrieve-a-page-property) with the page and property IDs to retrieve one property. Use it when a formula, rollup, or relation contains more than 25 references.
Don't fetch every property for every result unless you need all of them.
If you're still running into long query times with this API, other tips include:
* Using more specific filter conditions to reduce the result set, e.g. a more specific title query or a shorter time window.
* Dividing large data sources (ones with more than several dozen thousand pages) into multiple; e.g. splitting a "tasks" database into "Tasks" and "Bugs".
* Pruning data source schemas to remove any complex formulas, rollups, two-way relations, or other properties that are no longer in use.
* Setting up [connection webhooks](/reference/webhooks) to reduce the need for polling this API by instead automatically notifying your system of incremental workspace events.
For more information, visit our [help center article on optimizing database load times](https://www.notion.com/help/optimize-database-load-times-and-performance).
### Other important details and tips
**Permissions**
Before a connection can query a data source, its parent database must be shared with the connection. Attempting to query a data source in a database that has not been shared will return an HTTP response with a 404 status code.
To share a database with a connection, click the ••• menu at the top right of a database page, scroll to `Add connections`, and use the search bar to find and select the connection from the dropdown list.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. Attempting to call this API without read content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
**To display the page titles of related pages rather than just the ID:**
1. Add a rollup property to the data source which uses a formula to get the related page's title. This works well if you have access to [update](/reference/update-a-data-source) the data source's schema.
2. Otherwise, [retrieve the individual related pages](/reference/retrieve-a-page) using each page ID.
**Formula and rollup limitations**
* If a formula depends on a page property that is a relation, and that relation has more than 25 references, only 25 will be evaluated as part of the formula.
* Rollups and formulas that depend on multiple layers of relations may not return correct results.
* Notion recommends individually [retrieving each page property item](/reference/retrieve-a-page-property) to get the most accurate result.
### Errors
Returns a 404 HTTP response if the data source doesn't exist, or if the connection doesn't have access to the data source.
Returns a 400 or a 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
Returns a 503 HTTP response if the data source query is temporarily unavailable due to backend datastore timeouts. The response body includes an `additional_data` object with retry guidance:
```json 503 response example theme={null}
{
"object": "error",
"status": 503,
"code": "service_unavailable",
"message": "Public API data source query is temporarily unavailable due to backend datastore timeouts. Retry with exponential backoff; if retries continue to fail, reduce page_size or narrow filters/sorts.",
"additional_data": {
"endpoint_name": "public_queryDataSource",
"notion_error_name": "PgPoolWaitConnectionTimeout",
"retry_guidance": [
"Use exponential backoff with jitter",
"Reduce page_size",
"Narrow query filters/sorts"
]
}
}
```
**Note**: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
# Query meeting notes
Source: https://developers.notion.com/reference/query-meeting-notes
post /v1/blocks/meeting_notes/query
Query meeting notes for the workspace with optional filters, sorts, and a result limit.
### Overview
Returns a list of **meeting notes** as [block objects](/reference/block) (`object` is `block`, `type` is `meeting_notes`) where the **user tied to the integration** (in the workspace) is listed as a attendee on the block.
The response contains:
* `results`: meeting note blocks (including `meeting_notes` payload with title, status, children tab IDs (e.g. summary, notes, transcription), and calendar and recording metadata when present).
* `has_more`: whether additional rows exist beyond this response for the current filter, sort, and `limit`.
**Field selection:** There is no field subset parameter—each `results[]` item is a full [block object](/reference/block). Read the fields you need (for example `meeting_notes`, timestamps, and people) from the response. Use `filter` to control **which** meeting notes are returned; allowed property names and operators are defined on this endpoint’s [request body schema](/reference/query-meeting-notes).
This endpoint does **not** use cursor-based pagination. There is no `start_cursor` or `next_cursor`—tune `filter`, `sort`, and `limit` (up to the maximum below) to refine the result set.
### Request body
The body is a JSON object; every field is optional.
| Field | Description |
| -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `filter` | A single **property** filter, or a **combinator** (`"and"` / `"or"`) with a `filters` array. See **Filtering**. |
| `sort` | Ordered list of sorts. Each entry has `property` and `direction` (`ascending` or `descending`). Earlier entries take precedence. |
| `limit` | Maximum number of meeting notes to return. **Integer** from **1** to **50**. If omitted, the server uses **50**. |
```json Default (implicit limit 50) theme={null}
{}
```
```json Sort and cap results theme={null}
{
"sort": [
{ "property": "last_edited_time", "direction": "descending" }
],
"limit": 10
}
```
### Filtering
A **filter** is either:
1. **Property filter (single condition)** — an object with `property` and `filter` (operator and optional `value`) at the **root** of the `filter` field.
2. **Combinator** — an object with `operator` (`"and"` or `"or"`) and a `filters` array. Each array element is another property filter or, for one level of nesting, a combinator whose inner `filters` are **property** filters only (see the [request schema](/reference/query-meeting-notes) in the API reference for the exact shape).
Property names, operators, and `value` shapes for text, date, and person filters are fully specified in the [request body schema](/reference/query-meeting-notes). Invalid properties or malformed filters return a **400** validation error.
### Filter examples
Three patterns below cover a **single** property filter, an **`and`** combinator, and an **`or`** combinator. For more properties and operators, use the schema link above. Replace sample strings and UUIDs with your own.
```json Single property (title) theme={null}
{
"filter": {
"property": "title",
"filter": {
"operator": "string_contains",
"value": { "type": "exact", "value": "standup" }
}
}
}
```
```json Combinator: and theme={null}
{
"filter": {
"operator": "and",
"filters": [
{
"property": "title",
"filter": {
"operator": "string_contains",
"value": { "type": "exact", "value": "planning" }
}
},
{
"property": "attendees",
"filter": { "operator": "is_not_empty" }
}
]
}
}
```
```json Combinator: or, with sort and limit theme={null}
{
"filter": {
"operator": "or",
"filters": [
{
"property": "title",
"filter": {
"operator": "string_contains",
"value": { "type": "exact", "value": "standup" }
}
},
{
"property": "title",
"filter": {
"operator": "string_contains",
"value": { "type": "exact", "value": "sprint" }
}
}
]
},
"sort": [
{ "property": "last_edited_time", "direction": "descending" }
],
"limit": 20
}
```
### Sorting
Each sort item has `property` and `direction` (`ascending` or `descending`). Property names are the same set as in the request body schema. **Earlier entries take precedence** when multiple sorts are present.
```json theme={null}
{
"sort": [
{ "property": "last_edited_time", "direction": "descending" },
{ "property": "title", "direction": "ascending" }
]
}
```
### Response shape
Each item in `results` is a meeting note block with a `meeting_notes` object plus the usual block metadata (see the response schema for this endpoint).
**Integration capabilities**
This endpoint requires an integration with **Read content**. The workspace must include **AI meeting notes** for the integration’s user; otherwise the call returns a validation error. See the [capabilities guide](/reference/capabilities).
### Errors
Returns a 400 HTTP response if AI meeting notes aren't available for the integration's user, or if the filter or sort is invalid.
Returns a 400 or a 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
**Note**: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
# Refresh a token
Source: https://developers.notion.com/reference/refresh-a-token
post /v1/oauth/token
Refreshes an access token, generating a new access token and new refresh token
For step-by-step instructions on how to use this endpoint to refresh an access token, check out the [Authorization guide](/guides/get-started/authorization#public-connection-auth-flow-set-up).
*Note: Each Public API endpoint can return several possible error codes. To see a full description of each type of error code, see the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation.*
# Request limits
Source: https://developers.notion.com/reference/request-limits
To ensure a consistent developer experience for all API users, the Notion API is rate limited and basic size limits apply to request parameters.
## Rate limits
The Notion API enforces two rate limits:
* **Per connection** — an average of three requests per second, with some bursts beyond the average allowed.
* **Per workspace** — shared across all of the workspace's connections and scaled to the workspace's plan.
Requests that exceed either limit return a `"rate_limited"` error code and an HTTP 429 response, with `additional_data.rate_limit_reason` indicating which limit was exceeded (for example, `public_api_request_rate_limit` or `public_api_space_request_rate_limit`).
Connections should handle HTTP 429 and 529 responses and respect the `Retry-After` response header. The header value is an integer number of seconds. A 529 response carries the `"service_overload"` code and means Notion is temporarily overloaded; retry it the same way as a 429.
### Retry rate-limited requests
Put outgoing requests through a queue so a burst from one job does not consume the connection's full request budget. When Notion returns 429 or 529:
1. Read `Retry-After` and pause new requests for at least that many seconds.
2. Retry the failed request after the pause.
3. If another 429 or 529 arrives, increase the delay with exponential backoff and jitter.
4. Set a retry limit. Log or surface the final error when the limit is reached.
Do not retry every error. Retry 429 and 529 responses. Retry 500, 502, 503, and 504 responses only when the request is idempotent, such as GET or DELETE, unless your application has its own idempotency protection. Fix the request before retrying most 400 responses. Treat 401 and 403 responses as authentication or authorization failures.
The JavaScript SDK retries 429 responses for every method. It also retries 500 and 503 responses for GET and DELETE requests. It respects `Retry-After`, uses exponential backoff with jitter, and limits retries. If you call the REST API directly, use the same safeguards and add explicit handling for 529 responses. These examples show the same policy in several common HTTP clients:
```js JavaScript theme={null}
async function notionRequest(url, options = {}, attempt = 0) {
const response = await fetch(url, options)
const method = (options.method ?? "GET").toUpperCase()
const isIdempotent = method === "GET" || method === "DELETE"
const retryable =
response.status === 429 ||
response.status === 529 ||
(isIdempotent && [500, 502, 503, 504].includes(response.status))
if (!retryable || attempt >= 5) {
return response
}
const retryAfter = response.headers.get("retry-after")
const retryAfterSeconds = Number(retryAfter)
const exponentialDelaySeconds = Math.min(2 ** attempt, 30)
const baseDelaySeconds = retryAfter !== null && Number.isFinite(retryAfterSeconds)
? retryAfterSeconds
: exponentialDelaySeconds
const jitterMs = Math.random() * 250
await new Promise(resolve =>
setTimeout(resolve, baseDelaySeconds * 1000 + jitterMs),
)
return notionRequest(url, options, attempt + 1)
}
```
```python Python theme={null}
import random
import time
import requests
def notion_request(method, url, *, max_attempts=6, **kwargs):
method = method.upper()
is_idempotent = method in {"GET", "DELETE"}
for attempt in range(max_attempts):
response = requests.request(method, url, **kwargs)
retryable = (
response.status_code in {429, 529}
or (
is_idempotent
and response.status_code in {500, 502, 503, 504}
)
)
if not retryable or attempt == max_attempts - 1:
return response
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 30)
time.sleep(delay + random.uniform(0, 0.25))
```
```go Go theme={null}
func notionRequest(client *http.Client, request *http.Request) (*http.Response, error) {
const maxAttempts = 6
isIdempotent := request.Method == http.MethodGet || request.Method == http.MethodDelete
for attempt := 0; attempt < maxAttempts; attempt++ {
attemptRequest := request.Clone(request.Context())
if attempt > 0 && request.Body != nil {
if request.GetBody == nil {
return nil, errors.New("request body cannot be replayed")
}
body, err := request.GetBody()
if err != nil {
return nil, err
}
attemptRequest.Body = body
}
response, err := client.Do(attemptRequest)
if err != nil {
return nil, err
}
retryableServerError := response.StatusCode == 500 ||
response.StatusCode == 502 || response.StatusCode == 503 ||
response.StatusCode == 504
retryable := response.StatusCode == 429 || response.StatusCode == 529 ||
(isIdempotent && retryableServerError)
if !retryable || attempt == maxAttempts-1 {
return response, nil
}
response.Body.Close()
delay := time.Duration(1< 30*time.Second {
delay = 30 * time.Second
}
if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay + time.Duration(rand.Intn(250))*time.Millisecond)
}
panic("unreachable")
}
```
```java Java theme={null}
HttpResponse notionRequest(
HttpClient client,
HttpRequest request
) throws IOException, InterruptedException {
int maxAttempts = 6;
boolean isIdempotent = Set.of("GET", "DELETE").contains(request.method());
for (int attempt = 0; attempt < maxAttempts; attempt++) {
HttpResponse response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
int status = response.statusCode();
boolean retryable = status == 429 || status == 529 ||
(isIdempotent && Set.of(500, 502, 503, 504).contains(status));
if (!retryable || attempt == maxAttempts - 1) {
return response;
}
long retryAfter = response.headers().firstValue("Retry-After")
.map(Long::parseLong)
.orElse(Math.min(1L << attempt, 30));
long jitterMillis = ThreadLocalRandom.current().nextLong(250);
Thread.sleep(retryAfter * 1000 + jitterMillis);
}
throw new IllegalStateException("unreachable");
}
```
The same rules apply in other languages: centralize retries in the HTTP client, respect `Retry-After`, add jitter, and cap the fallback delay and attempt count. Avoid independent retry loops in each worker; they can create a second traffic spike when the delay expires.
**Rate limits may change**
In the future, Notion plans to adjust rate limits to balance for demand and reliability.
## Size limits
Notion limits the size of certain parameters, and the depth of children in requests. A requests that exceeds any of these limits will return `"validation_error"` error code (HTTP response status 400) and contain more specific details in the `"message"` property.
Connections should avoid sending requests beyond these limits proactively. It may be helpful to use test data in your own test suite which intentionally contains large parameters to verify that the errors are handled appropriately. For example, if the connection reads a URL from an external system to put into a Notion page property, the connection should have a plan to deal with URLs that are beyond the length limit of 2000 characters. The connection might choose to log the error, or send an alert to the user who set up the connection via an email, or some other action.
Note that in addition to the property limits below, payloads have a maximum size of 1000 block elements and 500KB overall.
### Limits for property values
| Property value type | Inner property | Size limit |
| :---------------------------------------------------------------------------------------------------- | :-------------------- | :---------------- |
| [Rich text object](/reference/rich-text) | `text.content` | 2000 characters |
| [Rich text object](/reference/rich-text) | `text.link.url` | 2000 characters |
| [Rich text object](/reference/rich-text) | `equation.expression` | 1000 characters |
| Any array of all [block](/reference/block) types, including [rich text objects](/reference/rich-text) | | 100 elements |
| Any URL | | 2000 characters |
| Any email | | 200 characters |
| Any phone number | | 200 characters |
| Any multi-select | | 100 options |
| Any relation | | 100 related pages |
| Any people | | 100 users |
**Request size limits**
These cap the size of a single request, not how much a property can hold. A relation property can contain far more than 100 related pages — the limit only governs how many you add or set in one request. Responses have separate limits; use [Retrieve a page property item](/reference/retrieve-a-page-property) to paginate through large values.
# Retrieve a block
Source: https://developers.notion.com/reference/retrieve-a-block
get /v1/blocks/{block_id}
Retrieves a [Block object](/reference/block) using the ID specified.
If the block returned contains the key `has_children: true`, use the [Retrieve block children](/reference/get-block-children) endpoint to get the list of children.
To retrieve page content for a specific page, use [Retrieve block children](/reference/get-block-children) and set the page ID as the `block_id`.
For more information, read the [Working with page content guide](/guides/data-apis/working-with-page-content#modeling-content-as-blocks).
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. Attempting to call this API without read content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the block doesn't exist, or if the connection doesn't have access to the block.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Retrieve a data source
Source: https://developers.notion.com/reference/retrieve-a-data-source
get /v1/data_sources/{data_source_id}
Retrieves a [data source](/reference/data-source) object — information that describes the structure and columns of a data source — for a provided data source ID. The response adheres to any limits to a connection’s capabilities and the permissions of the data source and its containing database.
To fetch data source *rows* (i.e. the child pages of a data source) rather than columns, use the [Query a data source](/reference/query-a-data-source) endpoint.
### Finding a data source ID
First, find the containing database's ID in its Notion URL. The database ID is the 32-character alphanumeric string between the slash following the workspace name (if applicable) and the question mark.
Use [Retrieve a database](/reference/retrieve-database) with that database ID. Its `data_sources` array lists each data source's ID and name. Use the desired data source ID with this endpoint to retrieve its `properties`.
To get a data source ID from the Notion app directly, the settings menu for a database includes a "Copy data source ID" button under "Manage data sources":
Refer to the [Working with databases](/guides/data-apis/working-with-databases) guide for more details.
### Errors
Returns a 404 HTTP response if the data source doesn't exist or if the connection doesn't have access to it.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
### Additional resources
* [How to share a database with your connection](/guides/get-started/quick-start#give-your-connection-page-permissions)
* [Working with databases guide](/guides/data-apis/working-with-databases)
**Data source relations must be shared with your connection**
To retrieve data source properties from [database relations](https://www.notion.com/help/relations-and-rollups#what-is-a-database-relation), the related database must be shared with your connection in addition to the database being retrieved. If the related database is not shared, properties based on relations will not be included in the API response.
**The Notion API does not support retrieving linked data sources**
To fetch the information in a [linked data source](https://www.notion.com/help/guides/using-linked-databases), share the original source database with your Notion connection.
# Retrieve a database
Source: https://developers.notion.com/reference/retrieve-a-database
get /v1/databases/{database_id}
**Deprecated as of version 2025-09-03**
This page describes the API for versions up to and including `2022-06-28`. In the new `2025-09-03` version, the concepts of databases and data sources were split up, as described in [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
Refer to the new APIs instead:
* [Retrieve a database](/reference/retrieve-database)
* [Retrieve a data source](/reference/retrieve-a-data-source)
Retrieves a [database object](/reference/database) — information that describes the structure and columns of a database — for a provided database ID. The response adheres to any limits to a connection’s capabilities.
To fetch database rows rather than columns, use the [Query a database](/reference/post-database-query) endpoint.
To find a database ID, navigate to the database URL in your Notion workspace. The ID is the 32-character alphanumeric string between the slash following the workspace name (if applicable) and the question mark.
Refer to the [Working with databases](/guides/data-apis/working-with-databases) guide for more details.
### Errors
Returns a 404 HTTP response if the database doesn't exist or if the connection doesn't have access to it.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
### Additional resources
* [How to share a database with your connection](/guides/get-started/quick-start#give-your-connection-page-permissions)
* [Working with databases guide](/guides/data-apis/working-with-databases)
**Database relations must be shared with your connection**
To retrieve database properties from [database relations](https://www.notion.com/help/relations-and-rollups#what-is-a-database-relation), the related database must be shared with your connection in addition to the database being retrieved. If the related database is not shared, properties based on relations will not be included in the API response.
**The Notion API does not support retrieving linked databases.**
To fetch the information in a [linked database](https://www.notion.com/help/guides/using-linked-databases), share the original source database with your Notion connection.
# Retrieve a page
Source: https://developers.notion.com/reference/retrieve-a-page
get /v1/pages/{page_id}
**This endpoint will not accurately return properties that exceed 25 references**
Do **not** use this endpoint if a page property includes more than 25 references to receive the full list of references. Instead, use the [Retrieve a page property endpoint](/reference/retrieve-a-page-property) for the specific property to get its complete reference list.
Retrieves a [Page object](/reference/page) using the ID specified.
Responses contains page **properties**, not page content. To fetch page content, use the [Retrieve block children](/reference/get-block-children) endpoint.
Page properties are limited to up to **25 references** per page property. To retrieve data related to properties that have more than 25 references, use the [Retrieve a page property](/reference/retrieve-a-page-property#rollup-properties) endpoint. (See [Limits](/reference/retrieve-a-page#limits) below for additional information.)
### Parent objects: Pages vs. databases
If a page’s [Parent object](/reference/parent-object) is a database, then the property values will conform to the [database property schema](/reference/property-object).
If a page object is not part of a database, then the only property value available for that page is its `title`.
### Limits
The endpoint returns a maximum of 25 page or person references per [page property](/reference/page-property-values). If a page property includes more than 25 references, then the 26th reference and beyond might be returned as `Untitled`, `Anonymous`, or not be returned at all.
This limit affects the following properties:
* [`people`](/reference/page-property-values#people): response object can’t be guaranteed to return more than 25 people.
* [`relation`](/reference/page-property-values#relation): the `has_more` value of the `relation` in the response object is `true` if a `relation` contains more than 25 related pages. Otherwise, `has_more` is false.
* [`rich_text`](/reference/page-property-values#rich-text): response object includes a maximum of 25 populated inline page or person mentions.
* [`title`](/reference/page-property-values#title): response object includes a maximum of 25 inline page or person mentions.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. Attempting to call this API without read content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the page doesn't exist, or if the connection doesn't have access to the page.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Retrieve a page property item
Source: https://developers.notion.com/reference/retrieve-a-page-property
get /v1/pages/{page_id}/properties/{property_id}
Retrieves a `property_item` object for a given `page_id` and `property_id`. Depending on the property type, the object returned will either be a value or a [paginated](/reference/pagination) list of property item values. See [Property item objects](/reference/property-item-object) for specifics.
To obtain `property_id`s, use the [Retrieve a database](/reference/retrieve-a-database) endpoint.
In cases where a property item has more than 25 references, this endpoint should be used, rather than [Retrieve a page](/reference/retrieve-a-page). ([Retrieve a page](/reference/retrieve-a-page) will not return a complete list when the list exceeds 25 references.)
## Property Item Objects
For more detailed information refer to the [Property item object documentation](/reference/property-item-object)
### Simple Properties
Each individual `property_item` properties will have a `type` and under the the key with the value for `type`, an object that identifies the property value, documented under [Page property values](/reference/page-property-values).
### Paginated Properties
Property types that return a paginated list of property item objects are:
* `title`
* `rich_text`
* `relation`
* `people`
Look for the `next_url` value in the response object for these property items to view paginated results. Refer to [paginated page properties](/reference/page-property-values#paginated-page-properties) for a full description of the response object for these properties.
Refer to the [pagination reference](/reference/intro#pagination) for details on how to iterate through a results list.
### Rollup Properties
Learn more about rollup properties on the [Page properties page](/reference/page-property-values#rollup) or in Notion’s [Help Center](https://www.notion.com/help/relations-and-rollups).
For regular "Show original" rollups, the endpoint returns a flattened list of all the property items in the rollup.
For rollups with an aggregation, the API returns a [rollup property value](/reference/page-property-values#rollup) under the `rollup` key and the list of relations.
In order to avoid timeouts, if the rollup has a with a large number of aggregations or properties the endpoint returns a `next_cursor` value that is used to determinate the aggregation value *so far* for the subset of relations that have been paginated through.
Once `has_more` is `false`, then the final rollup value is returned. Refer to the [Pagination documentation](/reference/pagination) for more information on pagination in the Notion API.
Computing the values of following aggregations are *not* supported. Instead the endpoint returns a list of `property_item` objects for the rollup:
* `show_unique` (Show unique values)
* `unique` (Count unique values)
* `median`(Median)
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. Attempting to call this API without read content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the page or property doesn't exist, or if the connection doesn't have access to the page.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Retrieve a view
Source: https://developers.notion.com/reference/retrieve-a-view
get /v1/views/{view_id}
Retrieve a view by its ID.
For a successful request, the response is a [View](/reference/view) object.
**Connection capabilities**
This endpoint requires a connection to have read content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the view doesn't exist, or if the connection doesn't have access.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Retrieve an async task
Source: https://developers.notion.com/reference/retrieve-async-task
openapi.json GET /v1/async_tasks/{task_id}
Retrieve the status and result of an async task.
Use this endpoint to poll an `async_task` returned by an operation that was accepted for background execution.
The first async-capable REST endpoints are:
| Operation | Async support |
| ----------------------------------- | ----------------------------------------------------------------------- |
| `POST /v1/pages` | Supported only when the request includes the `markdown` body parameter. |
| `PATCH /v1/pages/:page_id/markdown` | Supported for markdown update requests. |
Set `allow_async: true` on a supported operation to opt into an `async_task` response. When `allow_async` is omitted or `false`, the endpoint keeps its existing synchronous response shape. `allow_async` changes response behavior only; it does not change validation, permissions, or the operation being performed.
Async task completion is polling-first in this version. Webhook notifications and ETA estimates are not part of the async task contract.
### Async task response
When an operation is accepted for background execution, the supported endpoint returns HTTP `202` with an `async_task` object:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "queued",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"poll_after_seconds": 2,
"operation": {
"surface": "rest",
"name": "PATCH /v1/pages/:page_id/markdown"
}
}
```
Use `status_url`, or call this endpoint with the returned `id`, to check completion.
### Status values
| Status | Meaning |
| ----------- | -------------------------------------------------------------------------------------------------------------- |
| `queued` | The task has been accepted and persisted, but processing has not started. |
| `running` | A worker is processing the task. |
| `retrying` | The task hit a retryable infrastructure or downstream-service failure and is scheduled to retry. |
| `succeeded` | The task completed successfully. The response includes a `result` object. |
| `failed` | The task failed terminally. The response includes an `error` object using the standard Public API error shape. |
For non-terminal statuses (`queued`, `running`, and `retrying`), wait at least `poll_after_seconds` before polling again.
Completed and failed task metadata is retained for a bounded period. After expiry, polling the task returns the standard not-found response, so store any final result data your application needs.
### Polling responses
An in-progress task includes the latest non-terminal status and polling guidance:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "running",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"poll_after_seconds": 2,
"operation": {
"surface": "rest",
"name": "PATCH /v1/pages/:page_id/markdown"
}
}
```
A successful task includes the operation result:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "succeeded",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"operation": {
"surface": "rest",
"name": "PATCH /v1/pages/:page_id/markdown"
},
"result": {
"object": "page_markdown",
"id": "page-uuid",
"markdown": "# Updated page\n\nThe update is complete.",
"truncated": false,
"unknown_block_ids": []
}
}
```
A failed task includes a standard Public API error object:
```json theme={null}
{
"object": "async_task",
"id": "task_abc123",
"status": "failed",
"status_url": "https://api.notion.com/v1/async_tasks/task_abc123",
"created_time": "2026-06-29T12:00:00.000Z",
"operation": {
"surface": "rest",
"name": "PATCH /v1/pages/:page_id/markdown"
},
"error": {
"object": "error",
"status": 400,
"code": "validation_error",
"message": "The request body was invalid."
}
}
```
### Errors
Returns a 404 HTTP response if the async task does not exist, has expired, or is not visible to the current connection.
Returns a 429 HTTP response if polling exceeds [request limits](/reference/request-limits). Malformed requests can return a 400 HTTP response.
*Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Retrieve a comment
Source: https://developers.notion.com/reference/retrieve-comment
get /v1/comments/{comment_id}
Retrieves a [Comment object](/reference/comment-object) from its `comment_id`.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**Reminder: Turn on connection comment capabilities**
Connection capabilities for reading and inserting comments are off by default.
This endpoint requires a connection to have read comment capabilities. Attempting to call this endpoint without read comment capabilities will return an HTTP response with a 403 status code.
For more information on connection capabilities, see the [capabilities guide](/reference/capabilities). To update your connection settings, visit the Developer portal.
# Retrieve a database
Source: https://developers.notion.com/reference/retrieve-database
get /v1/databases/{database_id}
Retrieves a [database object](/reference/database) — a container for one or more [data sources](/reference/data-source) — for a provided database ID. The response adheres to any limits to a connection’s capabilities.
The most important fields in the database object response to highlight:
* `data_sources`: An array of JSON objects with the `id` and `name` of every data source under the database
* These data source IDs can be used with the [Retrieve a data source](/reference/retrieve-a-data-source), [Update a data source](/reference/update-a-data-source), and [Query a data source](/reference/query-a-data-source) APIs
* `parent`: The direct parent of the database; generally a `page_id` or `workspace: true`
To find a database ID, navigate to the database URL in your Notion workspace. The ID is the 32-character alphanumeric string between the slash following the workspace name (if applicable) and the question mark.
Refer to the [Working with databases](/guides/data-apis/working-with-databases) guide for more details.
### Errors
Returns a 404 HTTP response if the database doesn't exist or if the connection doesn't have access to it.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
### Additional resources
* [How to share a database with your connection](/guides/get-started/quick-start#give-your-connection-page-permissions)
* [Working with databases guide](/guides/data-apis/working-with-databases)
# Retrieve a file upload
Source: https://developers.notion.com/reference/retrieve-file-upload
get /v1/file_uploads/{file_upload_id}
Use this API to get the details of a [File Upload](/reference/file-upload) object.
# Retrieve a page as markdown
Source: https://developers.notion.com/reference/retrieve-page-markdown
get /v1/pages/{page_id}/markdown
Retrieve the content of a page rendered as enhanced markdown.
### Use cases
Use this endpoint to retrieve the full content of a Notion page as [enhanced markdown](/guides/data-apis/enhanced-markdown), instead of working with the [block-based API](/reference/get-block-children). This is especially useful for agentic systems and developer tools that work natively with markdown.
The endpoint also accepts non-navigable block IDs returned in `unknown_block_ids` from a previous truncated response. Pass these IDs to fetch additional subtrees of a large page.
### General behavior
Returns a `page_markdown` object containing the page content as an enhanced markdown string.
**Requirements**
Your connection must have [read content capabilities](/reference/capabilities#content-capabilities) on the target page in order to call this endpoint. To update your connection's capabilities, navigate to the Developer portal, select your connection, open the **Configuration** tab, and scroll to the Capabilities section.
Attempting to call this endpoint without read content capabilities returns an HTTP response with a 403 status code.
### Unknown blocks
Some blocks may appear as `` tags in the markdown output. This happens when:
* **Truncation**: The page exceeds the record limit (approximately 20,000 blocks) and some blocks could not be loaded.
* **Permissions**: The page contains child pages or other content that is not shared with the connection.
* **Unsupported block types**: Certain block types (such as bookmarks, embeds, and link previews) are [not yet supported](/guides/data-apis/working-with-markdown-content#unsupported-block-types) in the markdown format.
When truncation or permissions cause unknown blocks, the `truncated` field is set to `true` and the `unknown_block_ids` array contains the affected block IDs.
You can attempt to fetch unloaded blocks by passing their IDs back to this same endpoint as the `page_id` path parameter. Blocks that are unknown due to permissions will return a 404 error since the connection does not have access.
The `unknown_block_ids` array does not distinguish between truncated and inaccessible blocks. Handle `object_not_found` errors gracefully when re-fetching unknown block IDs.
For unsupported block types, use the [block-based API](/reference/retrieve-a-block) to retrieve the full structured data.
### Errors
Returns a 404 HTTP response if the page doesn't exist, or if the connection doesn't have access to the page.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Revoke a token
Source: https://developers.notion.com/reference/revoke-token
post /v1/oauth/revoke
Revoke an access token.
# Rich text
Source: https://developers.notion.com/reference/rich-text
Notion uses rich text to allow users to customize their content. Rich text refers to a type of document where content can be styled and formatted in a variety of customizable ways. This includes styling decisions, such as the use of italics, font size, and font color, as well as formatting, such as the use of hyperlinks or code blocks.
Notion includes rich text objects in [block objects](/reference/block) to indicate how blocks in a page are represented. [Blocks](/reference/block) that support rich text will include a rich text object; however, not all block types offer rich text.
When blocks are retrieved from a page using the [Retrieve a block](/reference/retrieve-a-block) or [Retrieve block children](/reference/get-block-children) endpoints, an array of rich text objects will be included in the block object (when available). Developers can use this array to retrieve the plain text (`plain_text`) for the block or get all the rich text styling and formatting options applied to the block.
```json An example rich text object theme={null}
{
"type": "text",
"text": {
"content": "Some words ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Some words ",
"href": null
}
```
Many [block types](/reference/block#block-type-objects) support rich text. In cases where it is supported, a `rich_text` object will be included in the block `type` object. All `rich_text` objects will include a `plain_text` property, which provides a convenient way for developers to access unformatted text from the Notion block.
Each rich text object contains the following fields.
| Field | Type | Description | Example value |
| :-------------------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| `type` | `string` (enum) | The type of this rich text object. Possible type values are: `"text"`, `"mention"`, `"equation"`. | `"text"` |
| `text` \| `mention` \| `equation` | `object` | An object containing type-specific configuration.
Refer to the rich text type objects section below for details on type-specific values. | Refer to the rich text type objects section below for examples. |
| `annotations` | `object` | The information used to style the rich text object. Refer to the annotation object section below for details. | Refer to the annotation object section below for examples. |
| `plain_text` | `string` | The plain text without annotations. | `"Some words "` |
| `href` | `string` (optional) | The URL of any link or Notion mention in this text, if any. | `"https://app.notion.com/p/Avocado-d093f1d200464ce78b36e58a3f0d8043"` |
## The annotation object
All rich text objects contain an `annotations` object that sets the styling for the rich text. `annotations` includes the following fields:
| Property | Type | Description | Example value |
| :-------------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------ |
| `bold` | `boolean` | Whether the text is **bolded**. | `true` |
| `italic` | `boolean` | Whether the text is *italicized*. | `true` |
| `strikethrough` | `boolean` | Whether the text is struck through. | `false` |
| `underline` | `boolean` | Whether the text is underlined. | `false` |
| `code` | `boolean` | Whether the text is `code style`. | `true` |
| `color` | `string` (enum) | Color of the text. Possible values include:
- `"blue"` - `"blue_background"` - `"brown"` - `"brown_background"` - `"default"` - `"gray"` - `"gray_background"` - `"green"` - `"green_background"` - `"orange"` -`"orange_background"` - `"pink"` - `"pink_background"` - `"purple"` - `"purple_background"` - `"red"` - `"red_background”` - `"yellow"` - `"yellow_background"` | `"green"` |
## Rich text type objects
### Equation
Notion supports inline LaTeX equations as rich text object’s with a type value of `"equation"`. The corresponding equation type object contains the following:
| Field | Type | Description | Example value |
| ------------ | -------- | -------------------------------------------------- | ---------------------------------------------- |
| `expression` | `string` | The LaTeX string representing the inline equation. | `"\frac{{ - b \pm \sqrt {b^2 - 4ac} }}{{2a}}"` |
#### Example rich text `equation` object
```json JSON theme={null}
{
"type": "equation",
"equation": {
"expression": "E = mc^2"
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "E = mc^2",
"href": null
}
```
### Mention
Mention objects represent an inline mention of a database, date, link preview mention, page, template mention, or user. A mention is created in the Notion UI when a user types `@` followed by the name of the reference.
If a rich text object’s `type` value is `"mention"`, then the corresponding `mention` object contains the following:
| Field | Type | Description | Example value |
| :------------------------------------------------------------------------------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------ |
| `type` | `string` (enum) | The type of the inline mention. Possible values include:
- `"database"` - `"date"` - `"link_preview"` - `"page"` - `"template_mention"` - `"user"` | `"user"` |
| `database` \| `date` \| `link_preview` \| `page` \| `template_mention` \| `user` | `object` | An object containing type-specific configuration. Refer to the mention type object sections below for details. | Refer to the mention type object sections below for example values. |
#### Database mention type object
Database mentions contain a database reference within the corresponding `database` field. A database reference is an object with an `id` key and a string value (UUIDv4) corresponding to a database ID.
If a connection doesn’t have [access](/reference/capabilities) to the mentioned database, then the mention is returned with just the ID. The `plain_text` value that would be a title appears as `"Untitled"` and the annotation object’s values are defaults.
*Example rich text `mention` object for a `database` mention*
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "database",
"database": {
"id": "a1d8501e-1ac1-43e9-a6bd-ea9fe6c8822b"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Database with test things",
"href": "https://app.notion.com/p/a1d8501e1ac143e9a6bdea9fe6c8822b"
}
```
#### Date mention type object
Date mentions contain a [date property value object](/reference/page-property-values#date) within the corresponding `date` field.
*Example rich text `mention` object for a `date` mention*
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "date",
"date": {
"start": "2022-12-16",
"end": null
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "2022-12-16",
"href": null
}
```
#### Link Preview mention type object
If a user opts to share a [Link Preview](/guides/link-previews/introduction) as a mention, then the API handles the Link Preview mention as a rich text object with a `type` value of `link_preview`. Link preview rich text mentions contain a corresponding `link_preview` object that includes the `url` that is used to create the Link Preview mention.
*Example rich text `mention` object for a `link_preview` mention*
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "link_preview",
"link_preview": {
"url": "https://workspace.slack.com/archives/C04PF0F9QSD/z1671139297838409?thread_ts=1671139274.065079&cid=C03PF0F9QSD"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "https://workspace.slack.com/archives/C04PF0F9QSD/z1671139297838409?thread_ts=1671139274.065079&cid=C03PF0F9QSD",
"href": "https://workspace.slack.com/archives/C04PF0F9QSD/z1671139297838409?thread_ts=1671139274.065079&cid=C03PF0F9QSD"
}
```
#### Page mention type object
Page mentions contain a page reference within the corresponding `page` field. A page reference is an object with an `id` property and a string value (UUIDv4) corresponding to a page ID.
If a connection doesn’t have [access](/reference/capabilities) to the mentioned page, then the mention is returned with just the ID. The `plain_text` value that would be a title appears as `"Untitled"` and the annotation object’s values are defaults.
*Example rich text `mention` object for a `page` mention*
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "page",
"page": {
"id": "3c612f56-fdd0-4a30-a4d6-bda7d7426309"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "This is a test page",
"href": "https://app.notion.com/p/3c612f56fdd04a30a4d6bda7d7426309"
}
```
#### Template mention type object
The content inside a [template button](https://www.notion.com/help/template-buttons) in the Notion UI can include placeholder date and user mentions that populate when a template is duplicated. Template mention type objects contain these populated values.
Template mention rich text objects contain a `template_mention` object with a nested `type` key that is either `"template_mention_date"` or `"template_mention_user"`.
If the `type` key is `"template_mention_date"`, then the rich text object contains the following `template_mention_date` field:
| Field | Type | Description | Example value |
| :---------------------- | :-------------- | :---------------------------------------------------------------------------- | :------------ |
| `template_mention_date` | `string` (enum) | The type of the date mention. Possible values include: `"today"` and `"now"`. | `"today"` |
*Example rich text `mention` object for a `template_mention_date` mention*
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "template_mention",
"template_mention": {
"type": "template_mention_date",
"template_mention_date": "today"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "@Today",
"href": null
}
```
If the type key is `"template_mention_user"`, then the rich text object contains the following `template_mention_user` field:
| Field | Type | Description | Example value |
| :---------------------- | :-------------- | :--------------------------------------------------------------- | :------------ |
| `template_mention_user` | `string` (enum) | The type of the user mention. The only possible value is `"me"`. | `"me"` |
*Example rich text `mention` object for a `template_mention_user` mention*
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "template_mention",
"template_mention": {
"type": "template_mention_user",
"template_mention_user": "me"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "@Me",
"href": null
}
```
#### User mention type object
If a rich text object’s `type` value is `"user"`, then the corresponding user field contains a [user object](/reference/user).
If your connection doesn’t yet have access to the mentioned user, then the `plain_text` that would include a user’s name reads as `"@Anonymous"`. To update the connection to get access to the user, update the connection capabilities on the connection settings page.
*Example rich text `mention` object for a `user` mention*
```json JSON theme={null}
{
"type": "mention",
"mention": {
"type": "user",
"user": {
"object": "user",
"id": "b2e19928-b427-4aad-9a9d-fde65479b1d9"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "@Anonymous",
"href": null
}
```
### Text
If a rich text object’s `type` value is `"text"`, then the corresponding `text` field contains an object including the following:
| Field | Type | Description | Example value |
| :-------- | :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
| `content` | `string` | The actual text content of the text. | `"Some words "` |
| `link` | `object` (optional) | An object with information about any inline link in this text, if included.
If the text contains an inline link, then the object key is `url` and the value is the URL’s string web address.
If the text doesn’t have any inline links, then the value is `null`. | `{ "url": "https://developers.notion.com/" }` |
#### Example rich text `text` object without link
```json JSON theme={null}
{
"type": "text",
"text": {
"content": "This is an ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "This is an ",
"href": null
}
```
#### Example rich `text` text object with link
```json JSON theme={null}
{
"type": "text",
"text": {
"content": "inline link",
"link": {
"url": "https://developers.notion.com/"
}
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "inline link",
"href": "https://developers.notion.com/"
}
```
**Rich text object limits**
Refer to the request limits documentation page for information about [limits on the size of rich text objects](/reference/request-limits#limits-for-property-values).
# Search optimizations and limitations
Source: https://developers.notion.com/reference/search-optimizations-and-limitations
## Optimizations
Search works best when the request is as specific as possible. We recommend filtering by object (such as `page` or `database`) and providing a text `query` to narrow down results.
To speed up results, try reducing the `page_size`. The default `page_size` is 100.
Our implementation of the search endpoint includes an optimization where any pages or databases that are directly shared with a connection are guaranteed to be returned. If your use case requires pages or databases to immediately be available in search without an indexing delay, we recommend that you share relevant pages/databases with your connection directly.
## Limitations
The search endpoint works best when it's being used to query for pages and databases by name. It is not optimized for the following use cases:
* **Exhaustively enumerating through all the documents that a bot has access to in a workspace.** Search is not guaranteed to return everything, and the index may change as your connection iterates through pages and databases.
* **Searching or filtering within a particular database.** This use case is much better served by finding the database ID and using the [Query a data source](/reference/query-a-data-source) endpoint.
* **Immediate and complete results.** Search indexing is not immediate. If a connection performs a search quickly after a page is shared with the connection (such as immediately after a user performs OAuth), then the response may not contain the page.
* When a connection needs to present a user interface that depends on search results, we recommend including a *Refresh* button to retry the search. This will allow users to determine if the expected result is present or not, and give them a way to try again.
* **Listing content in the trash.** Trash results are search-index-backed and eventually consistent. Results include only content the connection can access.
# Sort data source entries
Source: https://developers.notion.com/reference/sort-data-source-entries
A sort is a condition used to order the entries returned from a data source query.
A [data source query](/reference/query-a-data-source) can be sorted by a property and/or timestamp and in a given direction. For example, a library data source can be sorted by the "Name of a book" (i.e. property) and in `ascending` (i.e. direction).
Here is an example of a sort on a data source property.
```json Sorting by "Name" property in ascending direction theme={null}
{
"sorts": [
{
"property": "created_time",
"direction": "ascending"
},
]
}
```
If you’re using the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js), you can apply this sorting property to your query like so:
```javascript JavaScript theme={null}
const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_API_KEY });
// replace with your own data source ID
const dataSourceId = 'd9824bdc-8445-4327-be8b-5b47500af6ce';
const sortedRows = async () => {
const response = await notion.dataSources.query({
database_id: databaseId,
sorts: [
{
property: "Name",
direction: "ascending"
}
],
});
return response;
}
```
Data source queries can also be sorted by two or more properties, which is formally called a nested sort. The sort object listed first in the nested sort list takes precedence.
Here is an example of a nested sort.
```json JSON theme={null}
{
"sorts": [
{
"property": "Food group",
"direction": "descending"
},
{
"property": "Name",
"direction": "ascending"
}
]
}
```
In this example, the data source query will first be sorted by "Food group" and the set with the same food group is then sorted by "Name".
## Sort object
### Property value sort
This sort orders the data source query by a particular property.
The sort object must contain the following properties:
| Property | Type | Description | Example value |
| :---------- | :-------------- | :------------------------------------------------------------------------------- | :-------------- |
| `property` | `string` | The name of the property to sort against. | `"Ingredients"` |
| `direction` | `string` (enum) | The direction to sort. Possible values include `"ascending"` and `"descending"`. | `"descending"` |
### Entry timestamp sort
This sort orders the data source query by the timestamp associated with a data source entry.
The sort object must contain the following properties:
| Property | Type | Description | Example value |
| :---------- | :-------------- | :------------------------------------------------------------------------------------------------------------ | :------------------- |
| `timestamp` | `string` (enum) | The name of the timestamp to sort against. Possible values include `"created_time"` and `"last_edited_time"`. | `"last_edited_time"` |
| `direction` | `string` (enum) | The direction to sort. Possible values include `"ascending"` and `"descending"`. | `"descending"` |
# Status codes
Source: https://developers.notion.com/reference/status-codes
Responses from the API use HTTP response codes to indicate general classes of success and error.
## Success codes
| HTTP status code | Description |
| ---------------- | ------------------------------------------ |
| 200 | Notion successfully processed the request. |
## Error codes
Error responses contain more detail about the error in the response body, in the `"code"` and `"message"` properties. Some error responses may also include an `"additional_data"` object with extra context, such as retry guidance or endpoint-specific details.
| HTTP status code | `"code"` | Description | `"message"` example |
| :--------------- | :---------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `"invalid_json"` | The request body could not be decoded as JSON. | `"Error parsing JSON body."` |
| 400 | `"invalid_request_url"` | The request URL is not valid. | `"Invalid request URL"` |
| 400 | `"invalid_request"` | This request is not supported. | `"Unsupported request: ."` |
| 400 | `"invalid_grant"` | The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. See [OAuth 2.0 documentation](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) for more information. | `"Invalid code: this code has been revoked."` |
| 400 | `"validation_error"` | The request body does not match the schema for the expected parameters. Check the `"message"` property for more details. | `"body failed validation: body.properties should be defined, instead was undefined."` |
| 400 | `"missing_version"` | The request is missing the required `Notion-Version` header. See [Versioning](/reference/versioning). | `"Notion-Version header failed validation: Notion-Version header should be defined, instead was undefined."` |
| 400 | `"invalid_beta"` | The `Notion-Beta` header is malformed, names an unknown beta, uses an unsupported revision, or is missing from an endpoint that requires it. See [Versioning](/reference/versioning#beta-versions). | `"Beta \"notion-as-code\" revision 2026-07-01 is no longer supported. Send Notion-Beta: notion-as-code-2026-07-31."` |
| 401 | `"unauthorized"` | The bearer token is not valid. | `"API token is invalid."` |
| 403 | `"restricted_resource"` | Given the bearer token used, the client doesn't have permission to perform this operation. | `"API token does not have access to this resource."` |
| 404 | `"object_not_found"` | Given the bearer token used, the resource does not exist. This error can also indicate that the resource has not been shared with owner of the bearer token. If the connection name is available, it will be included in the error message. | `"Could not find database with ID: be907abe-510e-4116-a3d1-7ea71018c06f. Make sure the relevant pages and databases are shared with your connection \"My Connection\"."` |
| 409 | `"conflict_error"` | The transaction could not be completed, potentially due to a data collision. Make sure the parameters are up to date and try again. We also use this HTTP status code in rare cases when our [File Upload](/reference/file-upload) third-party data storage provider has downtime and sending file contents failed. In this case, please retry the request later. | `"Conflict occurred while saving. Please try again."` |
| 429 | `"rate_limited"` | This request exceeds the number of requests allowed. Slow down and try again. [More details on rate limits](/reference/request-limits). | `"You have been rate limited. Please try again in a few minutes."` |
| 500 | `"internal_server_error"` | An unexpected error occurred. Reach out to [Notion support](https://www.notion.com/help). | `"Unexpected error occurred."` |
| 502 | `"bad_gateway"` | Notion encountered an issue while attempting to complete this request (e.g., failed to establish a connection with an upstream server). Please try again. | `"Bad Gateway"` |
| 503 | `"service_unavailable"` | Notion is unavailable. This can occur when the time to respond to a request takes longer than 60 seconds, the maximum request timeout. Please try again later. | `"Notion is unavailable, please try again later."` |
| 503 | `"database_connection_unavailable"` | Notion's database is unavailable or is not in a state that can be queried. Please try again later. | `"Notion is unavailable, please try again later."` |
| 504 | `"gateway_timeout"` | Notion timed out while attempting to complete this request. Please try again later. | `"Gateway Timeout"` |
| 529 | `"service_overload"` | Notion is temporarily overloaded. Respect the `Retry-After` response header and try again later. | `"Notion is temporarily overloaded. Please try again later."` |
# Trash a page
Source: https://developers.notion.com/reference/trash-page
The API does not support permanently deleting pages.
To trash a page via the API, send an [Update page](/reference/patch-page) request with the `in_trash` body param set to `true`. To restore a page from the trash, set `in_trash` to `false`.
**`archived` was removed in `2026-03-11`**
In API versions before `2026-03-11`, `archived` was a deprecated alias for `in_trash`. Starting with `2026-03-11`, only `in_trash` is accepted. If your connection uses an older API version and still sends `archived`, update to `in_trash` before upgrading.
## Example request: trash a Notion page
```bash cURL theme={null}
curl https://api.notion.com/v1/pages/60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7 \
-H ‘Authorization: Bearer ‘"$NOTION_API_KEY"’’ \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
-X PATCH \
--data ‘{
"in_trash": true
}’
```
If you are using Notion’s [JavaScript SDK](https://github.com/makenotion/notion-sdk-js) to interact with the REST API, use the `update` method available for Notion pages.
```javascript JavaScript theme={null}
const { Client } = require("@notionhq/client")
// Initializing a client
const notion = new Client({
auth: process.env.NOTION_API_KEY,
})
const trashPage = async () => {
await notion.pages.update({
page_id: pageId,
in_trash: true,
});
}
```
If successful, the API responds with a `200` HTTP status code and the trashed page object, as in the following example:
```json JSON expandable theme={null}
{
"object": "page",
"id": "be633bf1-dfa0-436d-b259-571129a590e5",
"created_time": "2022-10-24T22:54:00.000Z",
"last_edited_time": "2023-03-08T18:25:00.000Z",
"created_by": {
"object": "user",
"id": "c2f20311-9e54-4d11-8c79-7398424ae41e"
},
"last_edited_by": {
"object": "user",
"id": "9188c6a5-7381-452f-b3dc-d4865aa89bdf"
},
"cover": null,
"icon": {
"type": "emoji",
"emoji": "🐞"
},
"parent": {
"type": "database_id",
"database_id": "a1d8501e-1ac1-43e9-a6bd-ea9fe6c8822b"
},
"in_trash": true,
"properties": {
"Due date": {
"id": "M%3BBw",
"type": "date",
"date": {
"start": "2023-02-23",
"end": null,
"time_zone": null
}
},
"Status": {
"id": "Z%3ClH",
"type": "status",
"status": {
"id": "86ddb6ec-0627-47f8-800d-b65afd28be13",
"name": "Not started",
"color": "default"
}
},
"Title": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Bug bash",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Bug bash",
"href": null
}
]
}
},
"url": "https://www.notion.com/Bug-bash-be633bf1dfa0436db259571129a590e5"
}
```
Refer to the [error codes](/reference/status-codes#error-codes) documentation for possible errors.
## Example request: restore a Notion page
```bash cURL theme={null}
curl https://api.notion.com/v1/pages/60bdc8bd-3880-44b8-a9cd-8a145b3ffbd7 \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H "Content-Type: application/json" \
-H "Notion-Version: 2026-03-11" \
-X PATCH \
--data '{
"in_trash": false
}'
```
```javascript JavaScript theme={null}
// Restore a trashed page using the Notion JavaScript SDK
const restorePage = async () => {
await notion.pages.update({
page_id: pageId,
in_trash: false,
});
}
```
If successful, the API responds with a `200` HTTP status code and the restored [page object](/reference/page). Refer to the [error codes](/reference/status-codes#error-codes) documentation for possible errors.
# Unfurl attribute (Link Previews)
Source: https://developers.notion.com/reference/unfurl-attribute-object
A Link Preview is rendered from an array of unfurl attribute objects.
A [Link Preview](/guides/link-previews/introduction) is a real-time excerpt of authenticated content that unfurls in Notion when an authenticated user shares an enabled link. The display of a Link Preview can include structured content and rich media from the source service.
Custom Link Preview connections are no longer available for new developers to create. Existing supported Link Preview integrations continue to work for Notion users.
Link Previews can be displayed in their full format, or they can be shown as a "Mention".
Let's first look at an example of a full-format Link Preview:
Here is the same link again but now as a Mention — a miniature version of a Link Preview that uses the same data.
A Link Preview or Mention displays data that is sent to Notion as an array of unfurl attribute objects. There are a number of optional attributes. However, **every array must contain a `title` attribute and a `dev` attribute.**
Using the same Link Preview and Mention we saw above, let's look at the array of unfurl attribute objects that would render these previews. The following payload represents the example Link Preview and Mention above:
```json JSON expandable theme={null}
[
{
"id": "title",
"name": "Title",
"type": "inline",
"inline": {
"title": {
"value": "Feature Request: Link Previews",
"section": "title"
}
}
},
{
"id": "dev",
"name": "Developer Name",
"type": "inline",
"inline": {
"plain_text": {
"value": "Acme Inc",
"section": "secondary"
}
}
},
{
"id": "state",
"name": "State",
"type": "relation",
"relation": {
"uri": "acme:item_state/open",
"mention": {
"section": "primary"
}
}
},
{
"id": "itemId",
"name": "Item Id",
"type": "inline",
"inline": {
"plain_text": {
"value": "#23487",
"section": "identifier"
}
}
},
{
"id": "itemIcon",
"name": "Item Icon",
"type": "inline",
"inline": {
"color": {
"value": {
"r": 247,
"g": 247,
"b": 42
},
"section": "entity"
}
}
},
{
"id": "description",
"name": "Description",
"type": "inline",
"inline": {
"plain_text": {
"value": "Would love to be able to preview some Acme resources in Notion!\n Maybe an open item?",
"section": "body"
}
}
},
{
"id": "updated_at",
"name": "Updated At",
"type": "inline",
"inline": {
"datetime": {
"value": "2022-01-11T19:53:18.829Z",
"section": "secondary"
}
}
},
{
"id": "label",
"name": "Label",
"type": "inline",
"inline": {
"enum": {
"value": "🔨 Ready to Build",
"color": {
"r": 100,
"g": 100,
"b": 100
},
"section": "primary"
}
}
},
{
"id": "media",
"name": "Embed",
"embed": {
"src_url": "https://c.tenor.com/XgaU95K_XiwAAAAC/kermit-typing.gif",
"image": {
"section": "embed"
}
}
}
]
```
Each unfurl attribute object in this array maps to a different customizable section of a Link Preview. (To learn more about each section, jump to [The `section` value](/reference/unfurl-attribute-object#the-section-value).
First, let's let at the properties in each individual unfurl attribute object.
## The unfurl attribute object
```json Example unfurl attribute object theme={null}
{
"id": "title",
"name": "Title",
"type": "inline",
"inline": {
"title": {
"value": "Feature Request: Link Previews",
"section": "title"
}
}
}
```
Each unfurl attribute object contains the following values:
| Field | Type | Description | Example value |
| :-------------------- | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- |
| `id` | `string` | A unique identifier for the attribute. If more than one attribute with the same `id` is provided, then the latter attribute overrides the value of the first. | `"title"` |
| `name` | `string` | A human readable name describing the attribute. | `"Title"` |
| `type` | `inline` \|\| `embed` | The type of attribute. Most attributes are `inline`. Use `embed` for rich media sub-types like `image`, `video`, or `audio`. | `"inline"` |
| `inline` \|\| `embed` | `object` | An object whose key is a sub-type. The child sub-type object includes the `value` to display and the `section` of the Link Preview where the data is rendered. | `{ "title": { "value": "Feature Request: Link Previews", "section": "title" } }` |
### Inline sub-type objects
The key of inline sub-type objects represents the kind of sub-type. The values of the key are the `value` to display and the `section` of the Link Preview where the value is rendered.
| Sub-type | Description | Example value |
| :----------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| `color` | A color with r, b, g values. | `{ "value": { "r": 247, "g": 247, "b": 42 }, "section": "entity" }` |
| `date` | A date. | `{ "value": "2022-01-11", "section": "secondary" }` |
| `datetime` | A datetime. | `{ "value": "2022-01-11T19:53:18.829Z", "section": "secondary" }` |
| `enum` | A string value and optional color object. | `{ "value": "🔨 Ready to Build", "color": { "r": 100, "g": 100, "b": 100 }, "section": "primary" }` |
| `plain_text` | Any plain text content. | `{ "value": "Would love to be able to preview some Acme resources in Notion!\n Maybe an open item?", "section": "body" }` |
| `title`\* | The title of the Link Preview. \*An unfurl attribute object of this type must be included in every payload that renders a Link Preview. | `{ "value": "Feature Request: Link Previews", "section": "title" }` |
#### The `dev` attribute
Every array of attribute objects that is sent to Notion to render a Link Preview must also include a `dev` attribute. The attribute indicates the service or company associated with the Link Preview. It takes the following format:
```json Example dev attribute theme={null}
{
"id": "dev",
"name": "Developer Name",
"type": "inline",
"inline": {
"plain_text": {
"value": "Acme Inc",
"section": "secondary"
}
}
}
```
### Embed sub-type child objects
The `embed` sub-type object adds rich content like JPGs, GIFs, or iFrames to a Link Preview.
All embed sub-type objects contain: a `src_url` field that is a link to the embed, and an object whose key is the sub-type of the embed and whose value is an object indicating the `section` of the Link Preview where the value is rendered.
| Sub-type | Description | Example value |
| -------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `audio` | Audio from a source URL. | `{ "src_url": "https://s3.us-east-3.amazonaws.com/12345.mp4", "audio": { "section": "embed" } }` |
| `html` | HTML from a source URL that is rendered in an iFrame. | `{ "src_url": "https://s3.us-east-3.amazonaws.com/12345.html", "html": { "section": "embed" } }` |
| `image` | Image from a source URL. | `{ "src_url": "https://s3.us-east-3.amazonaws.com/12345.png", "image": { "section": "avatar" } }` |
| `video` | Video from a source URL. | `{ "src_url": "https://s3.us-east-3.amazonaws.com/12345.mp4", "video": { "section": "embed" } }` |
There’s no need to ask a user to log in to the source service in an iFrame embed. If they’re using a Link Preview, then they’ve already authenticated.
### The `section` value
The `section` value of an unfurl attribute object defines where an attribute is rendered in the Link Preview or Mention.
A `section` is specified in the sub-type object for the attribute. Refer to the table below for details about each `section` and its valid parent sub-types.
| Section | Description | Valid parent sub-types |
| :--------- | :--------------------------------------------------------------------------------------------- | :--------------------------------------- |
| avatar | The picture found on the bottom left of a Link Preview. | `image`, `plain_text` |
| background | A background color for the Link Preview. | `color` |
| body | The main string content of a Link Preview. | `plain_text` |
| embed | The large space where the content of an `embed` attribute type is displayed in a Link Preview. | `audio`, `html`, `image`, `pdf`, `video` |
| entity | The small picture found in the subheading of a Link Preview and in a Mention. | `color`, `image` |
| identifier | The subheading found on the bottom of a Link Preview and on the left side of a Mention. | `image`, `plain_text` |
| primary | The first subheading section. | `enum`, `date`, `datetime`, `plain_text` |
| secondary | The second subheading section. | `date`, `datetime`, `plain_text` |
| title\* | The main heading in a Link Preview or Mention. \*Required. | `title` |
# Update a block
Source: https://developers.notion.com/reference/update-a-block
patch /v1/blocks/{block_id}
Updates the content for the specified `block_id` based on the block type. Supported fields based on the block object type (see [Block object](/reference/block#block-type-objects) for available fields and the expected input for each field).
**Note**: The update replaces the *entire* value for a given field. If a field is omitted (ex: omitting `checked` when updating a `to_do` block), the value will not be changed.
**Updating `child_page` blocks**
To update `child_page` type blocks, use the [Update page](/reference/patch-page) endpoint. Updating the page's `title` updates the text displayed in the associated `child_page` block.
**Updating `child_database` blocks**
To update `child_database` type blocks, use the [Update database](/reference/update-a-database) endpoint. Updating the page's `title` updates the text displayed in the associated `child_database` block.
**Updating `children`**
A block's children *CANNOT* be directly updated with this endpoint. Instead use [Append block children](/reference/patch-block-children) to add children.
### Success
Returns a 200 HTTP response containing the updated [block object](/reference/block) on success.
**Connection capabilities**
This endpoint requires a connection to have update content capabilities. Attempting to call this API without update content capabilities will return an HTTP response with a 403 status code. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the block doesn't exist, is in the trash, or if the connection doesn't have access to the page.
Returns a 400 if the `type` for the block is incorrect or the input is incorrect for a given field.
Returns a 400 or a 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
*Note: Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Update comment
Source: https://developers.notion.com/reference/update-a-comment
patch /v1/comments/{comment_id}
Updates a comment by its `comment_id`.
Returns a [comment object](/reference/comment-object) for the updated comment.
A connection can only update comments that it created. Attempting to update a comment created by another user or connection will return a 404 error.
### Comment body format
The comment body can be provided in one of two formats:
* **`rich_text`**: An array of [rich text objects](/reference/rich-text) that represent the updated content of the comment.
* **`markdown`**: A Markdown string. Supports inline formatting (bold, italic, strikethrough, inline code, links), inline equations, and mentions.
Exactly one of `rich_text` or `markdown` must be provided. Providing both or neither will return a validation error.
### Errors
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**Reminder: Turn on connection comment capabilities**
Connection capabilities for reading and inserting comments are off by default.
This endpoint requires a connection to have insert comment capabilities. Attempting to call this endpoint without insert comment capabilities will return an HTTP response with a 403 status code.
For more information on connection capabilities, see the [capabilities guide](/reference/capabilities). To update your connection settings, visit the Developer portal.
# Update a data source
Source: https://developers.notion.com/reference/update-a-data-source
patch /v1/data_sources/{data_source_id}
Updates a [data source](/reference/data-source)'s title, icon, properties, parent, or trash status.
Returns the updated data source object.
Use the `parent` parameter to move the data source to a different `database_id`. If you do so, any existing views that refer to the data source in the current database continue to exist, but become *linked* views. A new standard "table" view for the moved data source is created under the new (destination) database. Use the Notion app to make any further changes to the views; managing views using the API is not currently supported.
Data source properties represent the columns (or schema) of a data source. To update the properties of a data source, use the `properties` [body param](/reference/update-data-source-properties) with this endpoint. Learn more about data source properties in the [data source properties](/reference/property-object) and [Update data source properties](/reference/update-data-source-properties) docs.
To update a `relation` data source property, share the related database with the connection. Learn more about relations in the [data source properties](/reference/property-object#relation) page.
For an overview of how to use the REST API with databases, refer to the [Working with databases](/guides/data-apis/working-with-databases) guide.
### How data sources property type changes work
All properties in pages are stored as rich text. Notion will convert that rich text based on the types defined in a data source's schema. When a type is changed using the API, the data will continue to be available, it is just presented differently.
For example, a multi select property value is represented as a comma-separated list of strings (eg. "1, 2, 3") and a people property value is represented as a comma-separated list of IDs. These are compatible and the type can be converted.
Note: Not all type changes work. In some cases data will no longer be returned, such as people type → file type.
### Interacting with data source rows
This endpoint cannot be used to update data source rows.
To update the properties of a data source row — rather than a column — use the [Update page](/reference/patch-page) endpoint. To add a new row to a database, use the [Create a page](/reference/post-page) endpoint.
### Recommended data source schema size limit
Developers are encouraged to keep their data source schema size to a maximum of **50KB**. To stay within this schema size limit, the number of properties (or columns) added to a data source should be managed.
Data source schema updates that are too large will be blocked by the REST API to help developers keep their data source queries performant. When a schema update is blocked, the error response includes a `validation_error` code with a message identifying the largest property by name, ID, and byte size to help you reduce your schema size.
### Errors
Returns a 404 HTTP response if the data source doesn't exist or if the connection doesn't have access to it.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**The following data source properties cannot be updated via the API:**
* `formula`
* [Synced content](https://www.notion.com/help/guides/synced-databases-bridge-different-tools)
* `place`
**Data source relations must be shared with your connection**
To update a data source [relation](https://www.notion.com/help/relations-and-rollups#what-is-a-database-relation) property, the related database must also be shared with your connection.
# Update a database
Source: https://developers.notion.com/reference/update-a-database
patch /v1/databases/{database_id}
**Deprecated as of version 2025-09-03**
>
This page describes the API for versions up to and including `2022-06-28`. In the new `2025-09-03` version, the concepts of databases and data sources were split up, as described in [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
Refer to the new APIs instead:
* [Update a database](/reference/update-database)
* [Update a data source](/reference/update-a-data-source)
Updates the database object — the title, description, or properties — of a specified database.
Returns the updated [database object](/reference/database).
Database properties represent the columns (or schema) of a database. To update the properties of a database, use the `properties` [body param](/reference/update-property-schema-object) with this endpoint. Learn more about database properties in the [database properties](/reference/property-object) and [Update database properties](/reference/update-property-schema-object) docs.
To update a `relation` database property, share the related database with the connection. Learn more about relations in the [database properties](/reference/property-object#relation) page.
For an overview of how to use the REST API with databases, refer to the [Working with databases](/guides/data-apis/working-with-databases) guide.
### How database property type changes work
All properties in pages are stored as rich text. Notion will convert that rich text based on the types defined in a database's schema. When a type is changed using the API, the data will continue to be available, it is just presented differently.
For example, a multi select property value is represented as a comma-separated list of strings (eg. "1, 2, 3") and a people property value is represented as a comma-separated list of IDs. These are compatible and the type can be converted.
Note: Not all type changes work. In some cases data will no longer be returned, such as people type → file type.
### Interacting with database rows
This endpoint cannot be used to update database rows.
To update the properties of a database row — rather than a column — use the [Update page](/reference/patch-page) endpoint. To add a new row to a database, use the [Create a page](/reference/post-page) endpoint.
### Recommended database schema size limit
Developers are encouraged to keep their database schema size to a maximum of **50KB**. To stay within this schema size limit, the number of properties (or columns) added to a database should be managed.
Database schema updates that are too large will be blocked by the REST API to help developers keep their database queries performant. When a schema update is blocked, the error response includes a `validation_error` code with a message identifying the largest property by name, ID, and byte size to help you reduce your schema size.
### Errors
Returns a 404 HTTP response if the database doesn't exist or if the connection doesn't have access to it.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
**The following database properties cannot be updated via the API:**
* `formula`
* `select`
* [Synced content](https://www.notion.com/help/guides/synced-databases-bridge-different-tools)
* A `multi_select` database property’s options values. An option can be removed, but not updated.
**Database relations must be shared with your connection**
To update a database [relation](https://www.notion.com/help/relations-and-rollups#what-is-a-database-relation) property, the related database must also be shared with your connection.
# Update a view
Source: https://developers.notion.com/reference/update-a-view
patch /v1/views/{view_id}
Update a view's name, filter, sorts, or configuration.
For a successful request, the response is the updated [View](/reference/view) object.
All body parameters are optional. Only the provided fields are updated; omitted fields are left unchanged. To clear a field, pass `null`.
**Connection capabilities**
This endpoint requires a connection to have update content capabilities. For more information on connection capabilities, see the [capabilities guide](/reference/capabilities).
### Errors
Returns a 404 HTTP response if the view doesn't exist, or if the connection doesn't have access.
Returns a 400 or 429 HTTP response if the request exceeds the [request limits](/reference/request-limits).
# Update data source properties
Source: https://developers.notion.com/reference/update-data-source-properties
The API represents columns of a data source in the Notion app UI as data source **properties**.
To use the API to update a data source's properties, send a [PATCH request](/reference/update-a-data-source) with a `properties` body param.
## Remove a property
To remove a data source property, set the property object to null.
```json removing properties by ID theme={null}
"properties": {
"J@cT": null,
}
```
```json removing properties by name theme={null}
"properties": {
"propertyToDelete": null
}
```
## Rename a property
To change the name of a data source property, indicate the new name in the `name` property object value.
```json renaming properties by ID theme={null}
"properties": {
"J@cT": {
"name": "New Property Name"
}
}
```
```json renaming properties by name theme={null}
"properties": {
"Old Property Name": {
"name": "New Property Name
}
}
```
| Property | Type | Description |
| :------- | :------- | :------------------------------------------------ |
| `name` | `string` | The name of the property as it appears in Notion. |
## Update property type
To update the property type, the property schema object should contain the key of the type. This type contains behavior of this property. Possible values of this key are `"title"`, `"rich_text"`, `"number"`, `"select"`, `"multi_select"`, `"status"`, `"date"`, `"people"`, `"files"`, `"checkbox"`, `"url"`, `"email"`, `"phone_number"`, `"formula"`, `"relation"`, `"rollup"`, `"created_time"`, `"created_by"`, `"last_edited_time"`, `"last_edited_by"`. Within this property, the configuration is a [property schema object](/reference/property-schema-object).
**Limitations**
Note that the property type of the `title` cannot be changed.
### Select configuration updates
To update an existing select configuration, the property schema object optionally contains the following configuration within the `select` property:
| Property | Type | Description | Example value |
| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `options` | optional array of [existing select options](#existing-select-options) and [select option objects](/reference/property-schema-object#select-options) | Settings for select properties. If an existing option is omitted, it will be removed from the data source property. New options will be added to the data source property. | |
#### Existing select options
Note that the name and color of an existing option cannot be updated.
| Property | Type | Description | Example value |
| :------- | :---------------- | :------------------ | :--------------------------------------- |
| `name` | optional `string` | Name of the option. | `"Fruit"` |
| `id` | optional `string` | ID of the option. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
### Multi-select configuration updates
To update an existing select configuration, the property schema object optionally contains the following configuration within the `multi_select` property:
| Property | Type | Description | Example value |
| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `options` | optional array of [existing select options](#existing-multi-select-options) and [multi-select option objects](/reference/property-schema-object#multi-select-options) | Settings for multi select properties. If an existing option is omitted, it will be removed from the data source property. New options will be added to the data source property. | |
#### Existing multi-select options
Note that the name and color of an existing option cannot be updated.
| Property | Type | Description | Example value |
| :------- | :---------------- | :------------------------------------------ | :--------------------------------------- |
| `name` | `string` | Name of the option as it appears in Notion. | `"Fruit"` |
| `id` | optional `string` | ID of the option. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
### Status configuration updates
To update an existing status configuration, the property schema object optionally contains the following configuration within the `status` property:
| Property | Type | Description | Example value |
| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `options` | optional array of [existing status options](#existing-status-options) and [status option objects](/reference/property-schema-object#status-options) | Settings for status properties. If an existing option is omitted, it will be removed from the data source property. New options will be added to the requested group when provided. | |
#### Existing status options
Note that the name and color of an existing option cannot be updated. Use `group` to assign an existing or new option to one of the existing status groups.
| Property | Type | Description | Example value |
| :------- | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `name` | optional `string` | Name of the option. | `"In progress"` |
| `id` | optional `string` | ID of the option. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
| `group` | optional `string` (enum) | Status group to assign the option to. Possible values are `To-do`, `In progress`, and `Complete`. Existing options keep their current group when omitted. New options use `To-do` when present, or the first existing group otherwise. | `"In progress"` |
When updating status options, group membership is kept consistent: removed options are automatically pruned from groups, and omitted `group` values preserve backwards-compatible placement. Groups themselves cannot be reconfigured via the API; use the Notion UI instead.
## Limitations
### Formula maximum depth
Formulas in Notion can have high levels of complexity beyond what the API can compute in a single request. For `formula` property values that exceed *have or exceed depth of 10* referenced tables, the API will return a "Formula depth" error as a [`"validation_error"`](/reference/errors)
As a workaround, you can retrieve the `formula` property object from the [Retrieve a data source](/reference/retrieve-a-data-source) endpoint and use the formula expression to compute the value of more complex formulas.
### Unsupported Rollup Aggregations
Due to the encoded cursor nature of computing rollup values, a subset of aggregation types are not supported. Instead the endpoint returns a list of *all* property\_item objects for the following rollup aggregations:
* `show_unique` (Show unique values)
* `unique` (Count unique values)
* `median` (Median)
### "Could not find page/data source" Error
A page property of type `rollup` and `formula` can involve computing a value based on the properties in another `relation` page. As such the connection needs permissions to the other `relation` page. If the connection doesn't have permissions page needed to compute the property value, the API will return a [`"object_not_found"`](/reference/errors) error specifying the page the connection lacks permissions to.
### Property value doesn't match UI after pagination
If a property value involves [pagination](/reference/pagination) and the underlying properties or pages used to compute the property value change whilst the connection is paginating through results, the final value will impacted and is not guaranteed to be accurate.
# Update a database
Source: https://developers.notion.com/reference/update-database
patch /v1/databases/{database_id}
Updates the attributes — the title, description, icon, or cover, etc. — of a specified database.
Returns the updated [database object](/reference/database).
To update the `properties` of the [data sources](/reference/data-source) under a database, use the [Update a data source](/reference/update-a-data-source) API starting from API version `2025-09-03`.
For an overview of how to use the REST API with databases, refer to the [Working with databases](/guides/data-apis/working-with-databases) guide.
Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.
# Update a page's content as markdown
Source: https://developers.notion.com/reference/update-page-markdown
patch /v1/pages/{page_id}/markdown
Insert or replace content in a page using enhanced markdown.
### Use cases
#### Updating content with search-and-replace (recommended)
Use the `update_content` command to make targeted edits using an array of search-and-replace operations. Each operation specifies an `old_str` to find and a `new_str` to replace it with. This is the recommended approach for making precise edits without rewriting the full page.
#### Replacing all page content (recommended)
Use the `replace_content` command to replace the entire page content with new markdown. Provide the full replacement content in `new_str`.
#### Inserting content (legacy)
Use the `insert_content` command to add new markdown content to a page. Provide `position: { "type": "start" }` to prepend, `position: { "type": "end" }` to explicitly append, or omit `position` to append to the end of the page. You can also provide an `after` selection to insert at a specific point; `after` uses an **ellipsis-based selection** format: `"start text...end text"`.
We recommend using `update_content` or `replace_content` instead. The `insert_content` command is still supported but may be deprecated in a future version.
#### Replacing a content range (legacy)
Use the `replace_content_range` command to replace a matched range of existing content with new markdown. The `content_range` parameter uses the same ellipsis-based selection format as `after`.
We recommend using `update_content` instead. The `replace_content_range` command is still supported but may be deprecated in a future version.
### General behavior
Returns a `page_markdown` object containing the full page content as enhanced markdown after the update, including `truncated` and `unknown_block_ids` fields for large pages.
Set `allow_async: true` at the top level of the request body to receive an HTTP `202` response with an `async_task` object instead of waiting for the markdown update to finish in the original request. This is useful for high block-count markdown updates, especially `replace_content` requests and large batches of `update_content` operations.
If `allow_async` is omitted or `false`, this endpoint keeps its existing synchronous response behavior. `allow_async` changes response behavior only; it does not change validation, permissions, or which operation runs. See [Retrieve an async task](/reference/retrieve-async-task) and [Working with markdown content](/guides/data-apis/working-with-markdown-content#running-large-markdown-writes-asynchronously) for polling examples.
**Requirements**
Your connection must have [update content capabilities](/reference/capabilities#content-capabilities) on the target page in order to call this endpoint. To update your connection's capabilities, navigate to the Developer portal, select your connection, open the **Configuration** tab, and scroll to the Capabilities section.
Attempting to call this endpoint without update content capabilities returns an HTTP response with a 403 status code.
**Newlines in content**
The `content` field expects standard markdown with actual newline characters. In JSON, `\n` is the escape sequence for a newline — for example, `"## Heading\n\nParagraph"` creates a heading followed by a paragraph.
When using cURL, wrap the `--data` body in **single quotes** (`'...'`) so that `\n` is passed through to the JSON parser. Avoid `$'...'` quoting, which converts `\n` into a literal newline and produces invalid JSON.
Note that the interactive API explorer on this page does not support multiline input. To test with newlines, use cURL, an SDK, or any HTTP client that sends properly encoded JSON.
**Protecting child pages and databases**
By default, this endpoint refuses to delete child pages or databases. If an operation would remove them, a `validation_error` is returned listing the affected items. Set `allow_deleting_content` to `true` in the command body (`replace_content_range`, `update_content`, or `replace_content`) to permit deletion.
### Errors
| Error code | Condition |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `validation_error` | The `content_range` or `after` selection does not match any content in the page, or an `old_str` in `update_content` is not found. |
| `validation_error` | Both `insert_content.after` and `insert_content.position` are provided. Use only one insertion target. |
| `validation_error` | The operation would delete child pages or databases and `allow_deleting_content` is not `true`. |
| `validation_error` | An `old_str` in `update_content` matches multiple locations and `replace_all_matches` is not `true`. |
| `validation_error` | The provided ID is a database or non-page block. |
| `validation_error` | The target page is a synced page. Synced pages cannot be updated. |
| `object_not_found` | The page does not exist or the connection does not have access to it. |
*Each Public API endpoint can return several possible error codes. See the [Error codes section](/reference/status-codes#error-codes) of the Status codes documentation for more information.*
# Update database properties
Source: https://developers.notion.com/reference/update-property-schema-object
**Deprecated as of version 2025-09-03**
This page describes the API for versions up to and including `2022-06-28`. In the new `2025-09-03` version, the concepts of databases and data sources were split up, as described in [Upgrading to 2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
Refer to the new page instead:
* [Update data source properties](/reference/update-data-source-properties)
The API represents columns of a database in the Notion UI as database **properties**.
To use the API to update a data source's properties, send a [PATCH request](/reference/update-a-database) with a `properties` body param.
## Remove a property
To remove a database property, set the property object to null.
```json removing properties by ID theme={null}
"properties": {
"J@cT": null,
}
```
```json removing properties by name theme={null}
"properties": {
"propertyToDelete": null
}
```
## Rename a property
To change the name of a database property, indicate the new name in the `name` property object value.
```json renaming properties by ID theme={null}
"properties": {
"J@cT": {
"name": "New Property Name"
}
}
```
```json renaming properties by name theme={null}
"properties": {
"Old Property Name": {
"name": "New Property Name
}
}
```
| Property | Type | Description |
| :------- | :------- | :------------------------------------------------ |
| `name` | `string` | The name of the property as it appears in Notion. |
## Update property type
To update the property type, the property schema object should contain the key of the type. This type contains behavior of this property. Possible values of this key are `"title"`, `"rich_text"`, `"number"`, `"select"`, `"multi_select"`, `"status"`, `"date"`, `"people"`, `"files"`, `"checkbox"`, `"url"`, `"email"`, `"phone_number"`, `"formula"`, `"relation"`, `"rollup"`, `"created_time"`, `"created_by"`, `"last_edited_time"`, `"last_edited_by"`. Within this property, the configuration is a [property schema object](/reference/property-schema-object).
**Limitations**
Note that the property type of the `title` cannot be changed.
### Select configuration updates
To update an existing select configuration, the property schema object optionally contains the following configuration within the `select` property:
| Property | Type | Description | Example value |
| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `options` | optional array of [existing select options](#existing-select-options) and [select option objects](/reference/property-schema-object#select-options) | Settings for select properties. If an existing option is omitted, it will be removed from the database property. New options will be added to the database property. | |
#### Existing select options
Note that the name and color of an existing option cannot be updated.
| Property | Type | Description | Example value |
| :------- | :---------------- | :------------------ | :--------------------------------------- |
| `name` | optional `string` | Name of the option. | `"Fruit"` |
| `id` | optional `string` | ID of the option. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
### Multi-select configuration updates
To update an existing select configuration, the property schema object optionally contains the following configuration within the `multi_select` property:
| Property | Type | Description | Example value |
| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `options` | optional array of [existing select options](#existing-multi-select-options) and [multi-select option objects](/reference/property-schema-object#multi-select-options) | Settings for multi select properties. If an existing option is omitted, it will be removed from the database property. New options will be added to the database property. | |
#### Existing multi-select options
Note that the name and color of an existing option cannot be updated.
| Property | Type | Description | Example value |
| :------- | :---------------- | :------------------------------------------ | :--------------------------------------- |
| `name` | `string` | Name of the option as it appears in Notion. | `"Fruit"` |
| `id` | optional `string` | ID of the option. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
### Status configuration updates
To update an existing status configuration, the property schema object optionally contains the following configuration within the `status` property:
| Property | Type | Description | Example value |
| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| `options` | optional array of [existing status options](#existing-status-options) and [status option objects](/reference/property-schema-object#status-options) | Settings for status properties. If an existing option is omitted, it will be removed from the database property. New options will be added to the database property and assigned to the requested group when provided. | |
#### Existing status options
Note that the name and color of an existing option cannot be updated. Use `group` to assign an existing or new option to one of the existing status groups.
| Property | Type | Description | Example value |
| :------- | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- |
| `name` | optional `string` | Name of the option as it appears in Notion. | `"In review"` |
| `id` | optional `string` | ID of the option. | `"ff8e9269-9579-47f7-8f6e-83a84716863c"` |
| `group` | optional `string` (enum) | Status group to assign the option to. Possible values are `To-do`, `In progress`, and `Complete`. Existing options keep their current group when omitted. New options use `To-do` when present, or the first existing group otherwise. | `"In progress"` |
## Limitations
### Formula maximum depth
Formulas in Notion can have high levels of complexity beyond what the API can compute in a single request. For `formula` property values that exceed *have or exceed depth of 10* referenced tables, the API will return a "Formula depth" error as a [`"validation_error"`](/reference/errors)
As a workaround, you can retrieve the `formula` property object from the Retrieve a Database endpoint and use the formula expression to compute the value of more complex formulas.
### Unsupported Rollup Aggregations
Due to the encoded cursor nature of computing rollup values, a subset of aggregation types are not supported. Instead the endpoint returns a list of *all* property\_item objects for the following rollup aggregations:
* `show_unique` (Show unique values)
* `unique` (Count unique values)
* `median` (Median)
### `Could not find page/database` Error
A page property of type `rollup` and `formula` can involve computing a value based on the properties in another `relation` page. As such the connection needs permissions to the other `relation` page. If the connection doesn't have permissions page needed to compute the property value, the API will return a [`"object_not_found"`](/reference/errors) error specifying the page the connection lacks permissions to.
### Property value doesn't match UI after pagination
If a property value involves [pagination](/reference/pagination) and the underlying properties or pages used to compute the property value change whilst the connection is paginating through results, the final value will impacted and is not guaranteed to be accurate.
# Send a file upload
Source: https://developers.notion.com/reference/upload-file
post /v1/file_uploads/{file_upload_id}/send
Use this API to transmit file contents to Notion for a [file upload](/reference/file-upload).
For this endpoint, use a `Content-Type` of `multipart/form-data`, and provide your file contents under the `file` key.
The use of multipart form data is unique to this endpoint. Other Notion APIs, including [Create a file upload](/reference/create-file) and [Complete a file upload](/reference/complete-file-upload), use JSON parameters.
Include a `boundary` with the `Content-Type` header of your request as per [RFC 2388](https://datatracker.ietf.org/doc/html/rfc2388). Most request libraries (e.g. `fetch`, `ky`) automatically handle this as long as you provide a form data object but don't overwrite the `Content-Type` explicitly.
For more tips and examples, view the [file upload guide](/guides/data-apis/uploading-small-files#step-2-upload-file-contents).
When `mode=multi_part`, each part must include a form field `part_number` to indicate which part is being sent. Parts may be sent concurrently up to standard Notion API [rate limits](/reference/request-limits), and may be sent out of order as long as all parts (1, ..., `part_number`) are successfully sent before calling the [complete file upload API](/reference/complete-file-upload).
The maximum allowed length of a file name is 900 bytes, including any file extension included in the file name or inferred based on the `content_type`. However, we recommend using shorter names for performance and easier file management and lookup using the [List file uploads](/reference/list-file-uploads) API.
# User
Source: https://developers.notion.com/reference/user
The User object represents a user in a Notion workspace. Users include full workspace members, guests, and connections. You can find more information about members and guests in [this guide](https://www.notion.com/help/add-members-admins-guests-and-groups).
**Provisioning users and groups using SCIM**
The SCIM API is available for workspaces in Notion's Enterprise Plan. Learn more about [using SCIM with Notion](https://www.notion.com/help/provision-users-and-groups-with-scim).
**Setting up single sign-on (SSO) with Notion**
Single sign-on (SSO) can be configured for workspaces in Notion's Enterprise Plan. [Learn more about SSO with Notion](https://www.notion.com/help/saml-sso-configuration).
## Where user objects appear in the API
User objects appear in nearly all objects returned by the API, including:
* [Block object](/reference/block) under `created_by` and `last_edited_by`.
* [Page object](/reference/page) under `created_by` and `last_edited_by` and in `people` property items.
* [Database object](/reference/database) under `created_by` and `last_edited_by`.
* [Rich text object](/reference/rich-text), as user mentions.
* [Property object](/reference/property-object) when the property is a `people` property.
User objects will **always** contain `object` and `id` keys, as described below. The remaining properties may appear if the user is being rendered in a rich text or page property context, and the bot has the correct capabilities to access those properties. For more about capabilities, see the [Capabilities guide](/reference/capabilities) and the [Authorization guide](/guides/get-started/authorization).
## All users
These fields are shared by all users, including people and bots. Fields marked with \* are always present.
| Property | Updatable | Type | Description | Example value |
| :----------- | :----------- | :------------------------ | :------------------------------------------------------------ | :---------------------------------------------------------------------------- |
| `object`\* | Display-only | `"user"` | Always "user" | `"user"` |
| `id`\* | Display-only | `string` (UUID) | Unique identifier for this user. | `"e79a0b74-3aba-4149-9f74-0bb5791a6ee6"` |
| `type` | Display-only | `string` (optional, enum) | Type of the user. Possible values are `"person"` and `"bot"`. | `"person"` |
| `name` | Display-only | `string` (optional) | User's name, as displayed in Notion. | `"Avocado Lovelace"` |
| `avatar_url` | Display-only | `string` (optional) | Chosen avatar image. | `"https://secure.notion-static.com/e6a352a8-8381-44d0-a1dc-9ed80e62b53d.jpg"` |
## People
User objects that represent people have the `type` property set to `"person"`. These objects also have the following properties:
| Property | Updatable | Type | Description | Example value |
| :---------------------- | :----------- | :-------- | :------------------------------------------------------------------------------------------------------------------------ | :-------------------- |
| `person` | Display-only | `object` | Properties only present for non-bot users. | |
| `person.email` | Display-only | `string` | Email address of person. This is only present if a connection has user capabilities that allow access to email addresses. | `"[email protected]"` |
| `person.email_verified` | Display-only | `boolean` | Whether Notion has verified the person's email address. Returned under the same capability conditions as `person.email`. | `true` |
## Bots
A user object's `type` property is `"bot"` when the user object represents a bot. A bot user object has the following properties:
| Property | Updatable | Type | Description | Example value |
| :----------------------------------------------- | :----------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bot` | Display-only | `object` | If you're using `GET /v1/users/me` or `GET /v1/users/{{your_bot_id}}`, then this field returns data about the bot, including `owner`, `owner.type`, and `workspace_name`. These properties are detailed below. | `{ "object": "user", "id": "9188c6a5-7381-452f-b3dc-d4865aa89bdf", "name": "Test Connection", "avatar_url": null, "type": "bot", "bot": { "owner": { "type": "workspace", "workspace": true }, "workspace_name": "Ada Lovelace’s Notion" } }` |
| `owner` | Display-only | `object` | Information about who owns this bot. | `{ "type": "workspace", "workspace": true }` |
| `owner.type` | Display-only | `string` enum | The type of owner, either `"workspace"` or `"user"`. | `"workspace"` |
| `workspace_name` | Display-only | `string` enum | If the `owner.type` is `"workspace"`, then `workspace.name` identifies the name of the workspace that owns the bot. If the `owner.type` is `"user"`, then `workspace.name` is `null`. | `"Ada Lovelace’s Notion"` |
| `workspace_id` | Display-only | `string` | ID of the bot's workspace. | `"17ab3186-873d-418f-b899-c3f6a43f68de"` |
| `workspace_limits` | Display-only | `object` | Information about the limits and restrictions that apply to the bot's workspace. | `{"max_file_upload_size_in_bytes": 5242880}` |
| `workspace_limits.max_file_upload_size_in_bytes` | Display-only | `integer` | The maximum allowable size of a [file upload](/reference/file-upload), in bytes. | `5242880` |
# Versioning
Source: https://developers.notion.com/reference/versioning
Learn about API change management and how to set the appropriate version in your connection.
The Notion API is versioned. Our API versions are named for the date the version is released. For example, our latest version is .
Set the version by including a `Notion-Version` header. Setting this header is **required**.
```bash cURL theme={null}
curl https://api.notion.com/v1/users/01da9b00-e400-4959-91ce-af55307647e5 \
-H "Authorization: Bearer secret_t1CdN9S8yicG5eWLUOfhcWaOscVnFXns"
-H "Notion-Version: 2026-03-11"
```
```javascript JavaScript theme={null}
// If you're using the JavaScript SDK, the appropriate
// Notion-Version header will be set for you.
const { Client } = require('@notionhq/client');
// To override the default version, pass the `notionVersion` argument
// when initializing the `Client`. For example:
//
// const notion = new Client({
// auth: process.env.NOTION_ACCESS_TOKEN,
// notionVersion: "2026-03-11"
// });
const notion = new Client({ auth: process.env.NOTION_ACCESS_TOKEN });
```
A new API version is released when we introduce a **backwards-incompatible** change to the API. For example, changing a property type's name.
```json JSON theme={null}
// Prior to version 2021-05-13, the rich text property is called "text"
"properties": {
"Description": {
"type": "text",
"text": [/* ... */]
}
}
// In version 2021-05-13, the rich text property is now called "rich_text"
"properties": {
"Description": {
"type": "rich_text",
"rich_text": [/* ... */]
}
}
```
In the above example, if you do not upgrade to the new version, you will continue to set text properties using `text` when creating or updating a page. Once you upgrade to the new version, you will need to use `rich_text` to set that same text property.
Similarly, the page response will be returned with the property type `text` on the old version, while on the new version, the response will say `rich_text`.
**Required Header**
The `Notion-Version` header must be included in all REST API requests. This ensures the Notion API response is consistent with what your code expects.
The most recent `Notion-Version` is .
**Versioning is only for backwards incompatible changes**
For new features and additions to the API, such as adding a new API endpoint, or including a new object in an existing API endpoint's response, there won't be a new version. You'll be able to take advantage of any new functionality on the version of the API you're currently using.
### What we consider backwards compatible
The following types of changes are made without a new API version. Your integration should be resilient to them:
* **Additive changes** — new endpoints, new optional request parameters, new fields in response objects. Additive changes apply to **every** API version at the same time, including older ones: pinning `Notion-Version` does not delay them.
* **Link format changes** — the links Notion generates for its own records (`url` on pages, databases, and data sources, and `href` on page and database mentions) are for people to open, not stable identifiers. Their domain and path format may change; for example, they moved from `https://www.notion.so/{page-id}` to `https://app.notion.com/p/{page-id}` in June 2026. Use `id` fields to reference records instead of parsing these URLs. Links authored by users, such as `link.url` in rich text and URL property values, are returned as written.
* **Opaque identifier format changes** — pagination cursors, request IDs, and other values not documented as stable identifiers may change in length, format, or structure at any time. Cursors may embed metadata such as session keys or timestamps. **Do not parse, validate, or store cursors beyond passing them back as `start_cursor`.** Only record identifiers (page IDs, database IDs, user IDs, block IDs, etc.) are guaranteed to be stable UUIDs.
* **Error message text improvements** — human-readable `message` strings in error responses may be reworded for clarity. Programmatic error `code` values (e.g. `validation_error`, `object_not_found`) are stable and will not change without a version bump.
* **Rate limit adjustments** — rate limits may change. Always respect `Retry-After` headers.
* **Performance and ordering improvements** — result ordering for endpoints that don't guarantee a specific sort order may change. Pagination behavior may change to improve reliability.
**Ignore unknown response fields**
Because new response fields appear on every API version as soon as they ship, deserialize API responses with a parser that ignores unrecognized fields. Strict parsing that rejects unknown keys will eventually break, and the risk is highest for clients that are slow to update, such as packaged desktop or mobile apps and device firmware. The same applies to Notion MCP OAuth and tool responses, which are not versioned with `Notion-Version` at all.
**Treat all cursors as opaque**
The `next_cursor` value returned by paginated endpoints may contain session metadata beyond a simple identifier. Always pass it back verbatim as `start_cursor` — do not parse, modify, or validate its format.
**Note:** You may notice that Notion API URLs contain a `v1`. This is not related to the versioning described above. We don't intend to change these URLs.
## Beta versions
Some endpoints are available as opt-in betas. To use one, send the required `Notion-Beta` flag alongside `Notion-Version`:
```bash theme={null}
Notion-Beta: notion-as-code-2026-07-31
```
Each flag has a kebab-case name and revision date in the form `-`. You can enable multiple betas by separating their flags with commas.
Some existing endpoints use an opt-in beta to introduce a stricter contract without breaking current integrations. Requests without the flag keep the previous behavior. Requests with the current flag use the beta contract and must pass its stricter validation.
Beta revisions can include breaking changes. When that happens, we publish a changelog entry and update the flag's revision date; requests using an older revision then return an error that names the current flag. Unknown names, malformed flags, duplicate names, and unsupported revisions also return an error instead of being ignored.
When a beta becomes generally available, the header is no longer required. We accept and ignore its retired flag for a grace period so clients can remove it without breaking at launch.
## Frequently asked questions
Releases of the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js) are published to NPM as [`@notionhq/client`](https://www.npmjs.com/package/@notionhq/client)
and use a separate [semantic versioning](https://semver.org/) scheme.
View the [GitHub release notes](https://github.com/makenotion/notion-sdk-js/releases) for the latest version and changes over time. Some SDK changes are also
featured in the [API changelog](/page/changelog).
When upgrading to a new "minor" or "patch" version of the SDK, you generally won't need to make any code changes to your connection. These updates contain backwards-compatible
improvements and fixes.
However, new "major" versions of the SDK are not backwards-compatible, and you may need to make updates to your connection.
Some major releases drop support for older Notion API versions and update the default version used to make requests when no `notionVersion` is provided to
the SDK `Client` constructor.
For example, `v5.0.0` and above dropped support for `2022-06-28` and earlier. Starting with `v5.12.0`, the SDK also supports `2026-03-11`.
See the upgrade guides for [2026-03-11](/guides/get-started/upgrade-guide-2026-03-11) and [2025-09-03](/guides/get-started/upgrade-guide-2025-09-03).
See the [compatibility table](https://github.com/makenotion/notion-sdk-js?tab=readme-ov-file#requirements-and-compatibility) in the README to see which versions
of the SDK are compatible with which versions of the API.
We don't currently have any plans to stop supporting older API versions. If this changes in the future, we'll communicate this with all affected
users and provide a time window and migration guidance. However, we recommend upgrading to the latest version
to take advantage of the latest features and improvements.
# View
Source: https://developers.notion.com/reference/view
Learn about the Notion view object and its properties.
**Getting started**
View [Working with views](/guides/data-apis/working-with-views) for a comprehensive guide to creating and managing views.
**Views** define how pages in a [data source](/reference/data-source) are filtered, sorted, and displayed within a [database](/reference/database). Each view has its own type (table, board, calendar, etc.), filter, sort order, and layout configuration.
## Supported view types
| Type | Description |
| :---------- | :--------------------------------------------- |
| `table` | Rows-and-columns spreadsheet layout. |
| `board` | Kanban board grouped by a property. |
| `calendar` | Calendar layout grouped by a date property. |
| `timeline` | Gantt-style timeline layout. |
| `gallery` | Card grid with cover images. |
| `list` | Simple list layout. |
| `form` | Form view for data entry. |
| `chart` | Chart visualization. |
| `map` | Map view with location pins. |
| `dashboard` | Multi-widget dashboard containing other views. |
View-specific `configuration` is available for all view types except `dashboard`. Dashboard views use `rows` to define their layout instead. See [Working with views](/guides/data-apis/working-with-views#view-configuration) for configuration details per view type.
## Object fields
The response of View APIs like [Retrieve a view](/reference/retrieve-a-view) contains view objects with the following fields:
| Field | Type | Description |
| :------------------ | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `object` | `"view"` | Always `"view"`. |
| `id` | UUID | The ID of the view. |
| `parent` | Object | The parent database. Contains `type: "database_id"` and `database_id`. |
| `data_source_id` | String \| null | The ID of the data source this view is scoped to, or `null` for dashboard views. |
| `name` | String | The display name of the view. |
| `type` | String | One of the [supported view types](#supported-view-types). |
| `filter` | Object \| null | The [filter](/reference/filter-data-source-entries) applied to this view, or `null` if no filter is set. |
| `sorts` | Array \| null | The [sorts](/reference/sort-data-source-entries) applied to this view, or `null` if no sorts are set. |
| `configuration` | Object \| null | View-specific layout configuration, discriminated by `type`. See [Working with views](/guides/data-apis/working-with-views#view-configuration) for details. |
| `created_time` | String | ISO 8601 timestamp when the view was created. |
| `created_by` | Object \| null | Partial [user](/reference/user) who created the view. |
| `last_edited_time` | String | ISO 8601 timestamp when the view was last edited. |
| `last_edited_by` | Object \| null | Partial [user](/reference/user) who last edited the view. |
| `url` | String | Deep link to the view in Notion. |
| `dashboard_view_id` | String | Only present for widget views inside a dashboard. The ID of the parent dashboard view. |
# Webhooks
Source: https://developers.notion.com/reference/webhooks
Learn how your connection can automatically respond to workspace activity in real-time.
Webhooks let your connection receive real-time updates from Notion. Whenever a page or database changes, Notion sends a secure HTTP POST request to your webhook endpoint. This allows your application to respond to workspace activity as it happens — whether that's syncing data, triggering automation, or keeping your UI in sync with user activity.
**Think of it like this:** Instead of repeatedly polling the Notion API to check if anything has changed, Notion will tell you the moment something important happens.
## How webhooks work: A simple example
**Let’s walk through an example from start to finish:**
Your connection is subscribed to `page.content_updated` events.
A user edits the title of a page in Notion.
Within a minute, Notion sends a webhook request to your configured endpoint.
The event payload includes metadata such as the page ID, the event type, and a timestamp.
Your server receives the event, verifies it, and calls the Notion API to fetch the updated title using the page ID from the event.
Your application updates its internal data or takes any other action you’ve defined.
This flow lets you react quickly to user activity, without polling or guessing when something has changed.
## Getting started with webhooks
### Step 1 - Creating a webhook subscription
To receive webhook events, you’ll need to create a subscription through your connection settings.
**You’ll need to:**
Visit your connection settings.
Either create a new connection or select an existing one.
Navigate to the **Webhooks** tab and click **+ Create a subscription**.
Enter your public **Webhook URL** — this is the public endpoint where you want Notion to send events. It must be a secure (SSL) and publicly available endpoint. Endpoints in localhost are not reachable.
Choose which event types you'd like to subscribe to. You can modify these later if needed.
Click **Create subscription**.
At this point, your webhook is created but not yet verified. To complete the setup, you’ll need to confirm that your endpoint can receive and respond to verification.
### Step 2 - Verifying the subscription
When you create a subscription, Notion sends a one-time POST request to your webhook URL. The body of the request contains a `verification_token`, which proves that Notion can successfully reach your endpoint.
**Example payload with `verification_token`**:
```json JSON theme={null}
{
"verification_token": "secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl"
}
```
**You’ll need to:**
Inspect the incoming request at your endpoint and extract the `verification_token` from the JSON payload.
1. (Optional): Securely store this token for payload validation setup later, [in step 3](#step-3-validating-event-payloads-recommended).
Go back to the **Webhooks** tab within your Notion connection UI and click **⚠️ Verify** on the bottom left of the page
Paste the `verification_token` value into the form and click **Verify subscription.**
If you did not receive a `verification_token`, you can click **Resend token** from the webhook verification modal.
Once submitted, your webhook subscription is considered active, and will start receiving events.
**Changing your webhook URL or event subscriptions**
You can only change the webhook URL before verification. After verification, if you need to change the URL, you must delete and recreate the subscription. You can change the subscribed events at any time.
### Step 3 - Validating event payloads (Recommended)
To help ensure the security of your connection, Notion includes a cryptographic signature with every webhook event we send. This allows you to verify that the payload was sent by Notion and hasn’t been modified in transit.
While payload validation is optional, we recommend implementing it for any production environment.
**Using a no-code or low-code platform?**
If you're using a no-code or low-code platform (like Zapier, Make, or Pipedream), you may not have access to custom code for signature verification — and that’s okay. Validation is encouraged, but not required for webhooks to work.
#### How it works
In the previous step, Notion sent a one-time `verification_token` to your webhook URL. You’ll use this token to verify the authenticity of all subsequent webhook events.
Every webhook request from Notion includes an `X-Notion-Signature` header, which contains an HMAC-SHA256 hash of the request body, signed with your `verification_token`.
**Sample `X-Notion-Signature` from Notion**:
```json JSON theme={null}
{
"X-Notion-Signature": "sha256=461e8cbcba8a75c3edd866f0e71280f5a85cbf21eff040ebd10fe266df38a735"
}
```
To validate the request, you can use the `verification_token` along with the event's payload to recompute the signature and verify the request's authenticity. If they match, the payload is trustworthy.
**Using the SDK for JavaScript and TypeScript?**
[`@notionhq/client`](https://github.com/makenotion/notion-sdk-js) v5.23.0 and later includes a `verifyWebhookSignature()` helper that performs this check for you, including the constant-time comparison. Pass the raw request body exactly as it arrived (re-serialized JSON produces different bytes and fails verification), the `X-Notion-Signature` header value, and your `verification_token`:
```javascript theme={null}
import { verifyWebhookSignature } from "@notionhq/client"
const isTrustedPayload = await verifyWebhookSignature({
body: rawRequestBody,
signature: request.headers["x-notion-signature"],
verificationToken,
})
```
It works without configuration in Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge Functions, and browsers. The SDK also exports `signWebhookPayload()` for generating signatures when testing your handler.
**Sample code for computing the signature and validating the webhook payload:**
```javascript JavaScript theme={null}
import { createHmac, timingSafeEqual } from "crypto"
// Retrieve the `verification_token` from the initial request
// (subscription verification; Step 2)
const verificationToken = "secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl"
// This body should come from your request body for subsequent validations
const body = {"verification_token":"secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl"}
const calculatedSignature = `sha256=${createHmac("sha256", verificationToken).update(JSON.stringify(body)).digest("hex")}`
const isTrustedPayload = timingSafeEqual(
Buffer.from(calculatedSignature),
Buffer.from(headers["X-Notion-Signature"]),
)
if (!isTrustedPayload) {
// Ignore the event
return
}
```
```python Python theme={null}
import hmac
import hashlib
import json
# Retrieve the `verification_token` from initial request
# (subscription verification; Step 2)
verification_token = "secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl"
# This body should come from your request body for subsequent validations
body = {"verification_token": "secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl"}
# Calculate the signature
body_json = json.dumps(body, separators=(",", ":")) # Minified JSON, matches JSON.stringify
hmac_obj = hmac.new(
verification_token.encode("utf-8"),
body_json.encode("utf-8"),
hashlib.sha256
)
calculated_signature = "sha256=" + hmac_obj.hexdigest()
# Assume headers is a dict containing HTTP headers
# Example:
# headers = {"X-Notion-Signature": ""}
# Use hmac.compare_digest for timing-safe comparison
is_trusted_payload = hmac.compare_digest(
calculated_signature,
headers["X-Notion-Signature"]
)
if not is_trusted_payload:
# Ignore the event
return
```
```ruby Ruby theme={null}
require 'openssl'
require 'json'
# Retrieve the verification_token from initial request
verification_token = "secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl"
# This body should come from your request body for subsequent validations
body = { "verification_token" => "secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl" }
# Calculate the signature (minified JSON to match JSON.stringify)
body_json = JSON.generate(body)
digest = OpenSSL::HMAC.hexdigest("SHA256", verification_token, body_json)
calculated_signature = "sha256=#{digest}"
# Assume headers is a Hash containing HTTP headers
# Example:
# headers = { "X-Notion-Signature" => "" }
# Constant-time comparison
is_trusted_payload = ActiveSupport::SecurityUtils.secure_compare(
calculated_signature,
headers["X-Notion-Signature"]
)
unless is_trusted_payload
# Ignore the event
return
end
```
Implementing this validation step is a small lift that adds a strong layer of security to your webhook connection. If you ever rotate or recreate your webhook subscription, be sure to update your stored `verification_token`.
## Testing your webhook subscription
Once your webhook subscription is set up and verified, it’s a good idea to test that everything is working as expected.
Below are three common test scenarios you can try, each corresponding to a supported event type. These tests simulate typical content updates and help ensure your endpoint is receiving and processing events correctly.
### Test 1 - Change a page title
This test checks your webhook’s ability to handle aggregated events, which are delivered with a short delay to avoid sending redundant updates.
**You’ll need to:**
In your Notion workspace, add the connection to a page.
Change the title of that page.
Wait a minute or two because aggregated events like `page.content_updated` are batched and may not be sent immediately.
Check your server logs or webhook handler. You should receive a `page.content_updated` event.
Use the entity.id value from the payload to call the `retrieve a page` endpoint and confirm the new title.
### Test 2 - Add a comment
This test checks event delivery for comments, which require specific capabilities.
**You’ll need to:**
In a page your connection has access to, add a new comment.
Your webhook should receive a `comment.created` event within a few seconds.
**Important:**
To receive this event, your connection must include the `comment read` capability in its configuration. You can confirm this by opening your connection's **Configuration** tab and scrolling to the Capabilities section.
### Test 3 - Modify a database schema
This test verifies that structural changes to databases are triggering events.
**You’ll need to:**
Open any database your connection is connected to.
Make a schema change — for example, add a new property (column), rename an existing one, or delete a property.
Your webhook should receive a `data_source.schema_updated` (in the new 2025-09-03 API version) or `database.schema_updated` (deprecated after 2022-06-28 API version) event shortly after the change.
## Troubleshooting tips
If your webhook isn’t receiving events as expected, here are a few things to double-check. These are the most common reasons developers miss events during setup or testing.
### 🔒 1. Check access permissions
Make sure the connection has access to the object that triggered the event. For example, if a new page is created inside a private page your connection doesn’t have access to, the event won’t be triggered.
### ✅ 2. Confirm capabilities
Some event types require specific capabilities to be enabled for your connection.
For instance, to receive `comment.created` events, your connection must have the "**comment read**" capability selected. Without it, even if your connection has access to the page, the comment event won’t be delivered.
You can view and update your connection’s capabilities in the **Capabilities** section of your connection settings.
### ⏳ 3. Understand aggregated event timing
Not all webhook events are sent immediately. Some, like page.content\_updated, are aggregated to reduce noise from frequent edits (e.g., typing, formatting, moving blocks). This is normal and helps group multiple rapid changes into a single webhook event.
See [Event Delivery ](/reference/webhooks-events-delivery#event-delivery)for a deeper explanation.
**Tip:**
If you're testing and expecting an instant response, start with non-aggregated events like `comment.created` or `page.locked`.
### ☑️ Confirm your subscription status
Even if everything else is configured correctly, your webhook won’t receive events unless the subscription is active.
Head to the **Webhooks** tab under your connection settings and make sure your subscription is **active**. If the status shows as **paused**, **pending verification**, or if the subscription was deleted, events won’t be delivered to your endpoint.
## Related resources
* [Event types & delivery](/reference/webhooks-events-delivery) — Full list of supported event types, payload structure, and delivery behavior.
* [Webhook event reference](/reference/webhooks/page-created) — API reference pages for each webhook event type with payload schemas.
# Event types & delivery
Source: https://developers.notion.com/reference/webhooks-events-delivery
Learn about the different event types and how they are delivered to your connection.
Webhooks currently notify you about changes to pages and databases — such as when a new page is created, a title is updated, or someone changes a database schema. The events themselves do not contain the full content that changed. Instead, the webhook acts as a signal that something changed, and it’s up to your connection to follow up with a call to the Notion API to retrieve the latest content.
For example, let’s say a user updates the title of a page. You’ll receive a `page.content_updated` webhook event with the ID of the page that changed. From there, your connection can use the [retrieve a page endpoint](/reference/retrieve-a-page) to fetch the latest page content — including the new title.
For detailed payload schemas for each webhook event type, see the [Webhook events](/reference/webhooks/page-created) API reference.
## Event types
### Event properties
**All webhook event types share the following shape of properties:**
| **Field** | **Type** | **Description** |
| :---------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | UUID | The unique ID of the webhook event |
| `timestamp` | String | ISO 8601 formatted time at which the event occurred. This field can be used to order events on your side |
| `workspace_id` | UUID | The workspace ID where the event originated from |
| `subscription_id` | UUID | The ID of the webhook subscription |
| `integration_id` | UUID | Associated connection ID the subscription is set up with |
| `type` | String | Type of the event, e.g. `page.created` |
| `authors` | Array | Array of objects with the ID (`id`) and type (`type`) of the author who performed the action. `type` can be `"person"`, `"bot"`, or `"agent"`. Typically an array of length 1; can be more for aggregated events. See [bot](/reference/user#bots) or [person](/reference/user#people) for details retrievable by ID in the [Users API](/reference/get-user). |
| `accessible_by` | Array | Array of objects with the ID (`id`) and type (`type`) of each accessible bot and user who owns the bot connection to the `integration_id` and has access to the webhook's `entity`. Only for public connections. `type` can be `"person"` or `"bot"`. |
| `attempt_number` | number | Attempt number (1-8) of the current event delivery |
| `entity` | Object | ID (`id`) and type (`type`) of the object that triggered the event. `type` can be `"page"`, `"block"`, or `"database"`. |
| `data` | Object | Additional, event-specific data. |
### Supported webhook event types
Notion currently supports the following webhook event types. Each event represents a meaningful change to content in a workspace — such as the creation of a page, a schema update, or a new comment.
**More event types may be added in the future**
If Notion supports additional event types or resources, your subscription won't update automatically to receive them.
To subscribe to more event types or change the existing types your endpoint is receiving, update your subscription in the connection page's **Webhooks** tab.
Below, you’ll find the list of available type values, a short description of what each event represents, and whether the event is aggregated. Aggregated events group multiple changes into a single notification to reduce noise and improve efficiency.
| Type | Description | Is aggregated? |
| :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- |
| `page.content_updated` | Triggered when the content of a page changes — for example adding or removing a block on the page. | Yes |
| `page.created` | Triggered when a new page is created. | Yes |
| `page.deleted` | Triggered when a page is moved to the trash. | Yes |
| `page.locked` | Triggered when a page is locked from editing. | No |
| `page.moved` | Triggered when a page is moved to another location. | Yes |
| `page.properties_updated` | Triggered when a page's property is updated. | Yes |
| `page.undeleted` | Triggered when a page is restored from the trash. | Yes |
| `page.unlocked` | Triggered when a page is unlocked | No |
| `database.content_updated` | Triggered when a database's content is updated— for example, adding or removing a child page.
**Deprecated** in 2025-09-03 API version. | Yes |
| `database.created` | Triggered when a new database is created. | Yes |
| `database.deleted` | Triggered when a database is moved to the trash. | Yes |
| `database.moved` | Triggered when a database is moved to another location. | Yes |
| `database.schema_updated` | Triggered when a database's schema is updated — for example, adding or removing a database property.
**Deprecated** in 2025-09-03 API version. | Yes |
| `database.undeleted` | Triggered when a database is restored from the trash. | Yes |
| `data_source.content_updated` | Triggered when a data source's content is updated— for example, adding or removing a child page.
**New** in 2025-09-03 API version. | Yes |
| `data_source.created` | Triggered when a new data source is created within an existing database.
**New** in 2025-09-03 API version. | Yes |
| `data_source.deleted` | Triggered when a data source is moved to the trash.
**New** in 2025-09-03 API version. | Yes |
| `data_source.moved` | Triggered when a data source is moved to another database.
**New** in 2025-09-03 API version. | Yes |
| `data_source.schema_updated` | Triggered when a data source's schema is updated — for example, adding or removing a database property.
**New** in 2025-09-03 API version. | Yes |
| `data_source.undeleted` | Triggered when a data source is restored from the trash.
**New** in 2025-09-03 API version. | Yes |
| `comment.created` | Triggered when a new comment or suggested edit is added to a page or block | No |
| `comment.deleted` | Triggered when a comment is deleted. | No |
| `comment.updated` | Triggered when a comment is edited. | No |
**What does “aggregated” mean?**
For high-frequency events like `page.content_updated`, Notion batches changes that occur within a short time window into a single webhook event. Events such as `page.created`, `page.deleted`, `page.undeleted` occur in quick succession, you may only recieve the most meaningful result event -- or none at all if the state returns to its original one.
Event aggregration helps reduce redundant calls and improves reliability. Aggregated events may have a slight delivery delay (typically under one minute).
## Event delivery
Events should be delivered within 5 minutes of their occurrences. Most should be be delivered within a minute. Here are a few things to keep in mind when consuming webhook events.
### Event aggregation
Certain events that occur frequently, like page.content\_updated, are aggregated by their entity within a brief time window. As a result, there may be a slight delay between the first occurrence of an event and its delivery to your webhook URL.
### Event ordering
Events may arrive in a different order than they occurred. If event ordering is critical for your workflows, use the event's timestamp field to reorder them. Also, webhook events may not show the most current state of the data. We strongly recommend fetching the latest data from the API.
### Delivery retries
We aim for at-most-once event delivery. If your webhook endpoint fails to acknowledge receipt of an event, we will retry delivery up to 8 times using an exponential backoff schedule. The final retry attempt occurs approximately 24 hours after the initial event trigger.
## Sample event payloads
Notion currently supports the following event `type`s. We've added an example payload to describe the shape of each event.
### page.created
```json JSON expandable theme={null}
{
"id": "367cba44-b6f3-4c92-81e7-6a2e9659efd4",
"timestamp": "2024-12-05T23:55:34.285Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.created",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-809d-8dc4-ff2d96ae3090",
"type": "page"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### page.properties\_updated
```json JSON expandable theme={null}
{
"id": "1782edd6-a853-4d4a-b02c-9c8c16f28e53",
"timestamp": "2024-12-05T23:57:05.379Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.properties_updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-809d-8dc4-ff2d96ae3090",
"type": "page"
},
"data": {
"parent": {
"id": "13950b26-c203-4f3b-b97d-93ec06319565",
"type": "space"
},
"updated_properties": ["XGe%40", "bDf%5B", "DbAu"]
}
}
```
### page.content\_updated
```json JSON expandable theme={null}
{
"id": "56c3e00c-4f0c-4566-9676-4b058a50a03d",
"timestamp": "2024-12-05T19:49:36.997Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.content_updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
},
"data": {
"updated_blocks": [
{
"id": "153104cd-477e-80ec-a87d-f7ff0236d35c",
"type": "block"
}
],
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### page.moved
```json JSON expandable theme={null}
{
"id": "7de99a6f-2edd-4116-bf59-2d09407bddec",
"timestamp": "2024-12-11T05:43:14.383Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.moved",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "154104cd-477e-8030-9989-d4daf352d900",
"type": "page"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### page.deleted
```json JSON expandable theme={null}
{
"id": "ea6b8136-1db6-4f2e-b157-84a532437f62",
"timestamp": "2024-12-05T23:59:31.215Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.deleted",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-8001-935c-c4b11828dfbd",
"type": "page"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### page.undeleted
```json JSON expandable theme={null}
{
"id": "ec37232c-a17b-4f02-bb7c-8d8e1f5f2250",
"timestamp": "2024-12-06T00:00:03.356Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.undeleted",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-8001-935c-c4b11828dfbd",
"type": "page"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### page.locked
```json JSON expandable theme={null}
{
"id": "e2a3092c-5af0-442f-9d11-b813145edb72",
"timestamp": "2024-12-06T00:00:56.480Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.locked",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-8001-935c-c4b11828dfbd",
"type": "page"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### page.unlocked
```json JSON expandable theme={null}
{
"id": "e2a3092c-5af0-442f-9d11-b813145edb72",
"timestamp": "2024-12-06T00:00:56.480Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.unlocked",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-8001-935c-c4b11828dfbd",
"type": "page"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### database.created
**Linked databases**
For [linked databases](https://www.notion.com/help/guides/using-linked-databases), the `entity.type` is `"block"` instead of `"database"`.
If you [retrieve](/reference/retrieve-a-block) this block in the API, it [has a type of `"child_database"`](/reference/block#child-database).
```json JSON expandable theme={null}
{
"id": "d0bd8927-0826-4db0-9e26-83d57253f1ff",
"timestamp": "2024-12-05T23:50:35.868Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "database.created",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80eb-ae76-e1c2a32c7b35",
"type": "database"
},
"data": {
"parent": {
"id": "153104cd-477e-803a-88dc-caececf26478",
"type": "page"
}
}
}
```
### database.content\_updated
**Deprecated in 2025-09-03 API version.**
```json JSON expandable theme={null}
{
"id": "25e44fe0-6785-45bb-adc2-a321526c12c5",
"timestamp": "2024-12-13T17:48:13.700Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "database.content_updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "15b104cd-477e-80c2-84a0-c32cefba5cff",
"type": "database"
},
"data": {
"updated_blocks": [
{
"id": "15b104cd-477e-80a4-bff3-cd05428a4d55",
"type": "block"
},
{
"id": "15b104cd-477e-80be-98e7-cdf0897fa5c9",
"type": "block"
}
],
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### database.moved
```json JSON expandable theme={null}
{
"id": "f9c70013-d79d-4c4e-8d5b-939429949a2e",
"timestamp": "2024-12-06T06:54:08.468Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "database.moved",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80eb-ae76-e1c2a32c7b35",
"type": "database"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### database.deleted
```json JSON expandable theme={null}
{
"id": "c00e2ea7-032a-4e20-ae05-d69028a09ae9",
"timestamp": "2024-12-05T23:51:27.295Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "database.deleted",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80eb-ae76-e1c2a32c7b35",
"type": "database"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### database.undeleted
```json JSON expandable theme={null}
{
"id": "edd8ff6e-0f07-4621-934b-76ca55129cc2",
"timestamp": "2024-12-05T23:52:16.149Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "database.undeleted",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80eb-ae76-e1c2a32c7b35",
"type": "database"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### database.schema\_updated
**Deprecated in 2025-09-03 API version.**
```json JSON expandable theme={null}
{
"id": "5496f509-6988-4bab-b6a9-bdce0b720ca0",
"timestamp": "2024-12-05T23:55:22.243Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "database.schema_updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80eb-ae76-e1c2a32c7b35",
"type": "database"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
},
"updated_properties": [
{
"id": "kqLW",
"name": "Created at",
"action": "created"
},
{
"id": "wX%7Bd",
"name": "Blurb",
"action": "updated"
},
{
"id": "LIM%5D",
"name": "Description",
"action": "deleted"
}
],
}
}
```
### data\_source.content\_updated
**New in 2025-09-03 API version. Replaces `database.content_updated`.**
```json JSON expandable theme={null}
{
"id": "25e44fe0-6785-45bb-adc2-a321526c12c5",
"timestamp": "2024-12-13T17:48:13.700Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "data_source.content_updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "15b104cd-477e-80c2-84a0-c32cefba5cff",
"type": "data_source"
},
"data": {
"updated_blocks": [
{
"id": "15b104cd-477e-80a4-bff3-cd05428a4d55",
"type": "block"
},
{
"id": "15b104cd-477e-80be-98e7-cdf0897fa5c9",
"type": "block"
}
],
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### data\_source.created
**New in 2025-09-03 API version.**
```json JSON expandable theme={null}
{
"id": "6f6469cb-8022-409d-a560-62e631a84d74",
"timestamp": "2025-09-03T17:24:49.997Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"attempt_number": 1,
"api_version": "2025-09-03",
"entity": {
"id": "263104cd-477e-804b-8c32-000b2fcd241a",
"type": "data_source"
},
"type": "data_source.created",
"data": {
"parent": {
"id": "153104cd-477e-803a-88dc-caececf26478",
"type": "page"
}
}
}
```
### data\_source.deleted
**New in 2025-09-03 API version.**
```json JSON expandable theme={null}
{
"id": "4e443c81-a332-40af-9300-c7eb6e514737",
"timestamp": "2025-09-03T17:54:38.833Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"attempt_number": 1,
"api_version": "2025-09-03",
"entity": {
"id": "263104cd-477e-804b-8c32-000b2fcd241a",
"type": "data_source"
},
"type": "data_source.deleted",
"data": {
"parent": {
"id": "263104cd-477e-80ef-8afe-c488d39a5cdb",
"type": "page"
}
}
}
```
### data\_source.moved
**New in 2025-09-03 API version.**
```json JSON expandable theme={null}
{
"id": "b6cc0a2c-f8f6-440b-920d-e3d6d7cf2e44",
"timestamp": "2025-09-03T17:49:13.978Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"attempt_number": 1,
"api_version": "2025-09-03",
"entity": {
"id": "263104cd-477e-8025-aae1-000b58fc5834",
"type": "data_source"
},
"type": "data_source.moved",
"data": {
"parent": {
"id": "263104cd-477e-80ef-8afe-c488d39a5cdb",
"type": "database"
}
}
}
```
### data\_source.schema\_updated
**New in 2025-09-03 API version. Replaces `database.schema_updated`.**
```json JSON expandable theme={null}
{
"id": "5496f509-6988-4bab-b6a9-bdce0b720ca0",
"timestamp": "2024-12-05T23:55:22.243Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "data_source.schema_updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80eb-ae76-e1c2a32c7b35",
"type": "data_source"
},
"data": {
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
},
"updated_properties": [
{
"id": "kqLW",
"name": "Created at",
"action": "created"
},
{
"id": "wX%7Bd",
"name": "Blurb",
"action": "updated"
},
{
"id": "LIM%5D",
"name": "Description",
"action": "deleted"
}
],
}
}
```
### data\_source.undeleted
**New in 2025-09-03 API version.**
```json JSON expandable theme={null}
{
"id": "afc2475f-aaeb-4a69-8158-b654ba4bc47b",
"timestamp": "2025-09-03T17:55:42.075Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"attempt_number": 1,
"api_version": "2025-09-03",
"entity": {
"id": "263104cd-477e-8025-aae1-000b58fc5834",
"type": "data_source"
},
"type": "data_source.undeleted",
"data": {
"parent": {
"id": "153104cd-477e-803a-88dc-caececf26478",
"type": "page"
}
}
}
```
### comment.created
**For page comment**:
```json JSON expandable theme={null}
{
"id": "c6780f24-10b7-4f42-a6fd-230b6cf7ad69",
"timestamp": "2024-12-05T20:46:45.854Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "comment.created",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80ca-8f75-001d9e2b6839",
"type": "comment"
},
"data": {
"page_id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
**For comment on a block**:
```json JSON expandable theme={null}
{
"id": "9cf67341-47d7-43f7-be6f-24b49dcc335b",
"timestamp": "2024-12-05T20:48:00.550Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "comment.created",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-8071-b16a-001d9a35ad84",
"type": "comment"
},
"data": {
"page_id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"parent": {
"id": "153104cd-477e-803a-88dc-caececf26478",
"type": "block"
}
}
}
```
### comment.updated
```json JSON expandable theme={null}
{
"id": "68ad06e4-5b68-498d-8812-9a1d3e069e46",
"timestamp": "2024-12-05T20:47:22.657Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "comment.updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-80ca-8f75-001d9e2b6839",
"type": "comment"
},
"data": {
"page_id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
```
### comment.deleted
```json JSON expandable theme={null}
{
"id": "aa4436d0-6694-49ad-aabb-55c6307f091b",
"timestamp": "2024-12-05T20:49:08.688Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "comment.deleted",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"accessible_by": [
{
"id": "556a1abf-4f08-40c6-878a-75890d2a88ba",
"type": "person"
},
{
"id": "1edc05f6-2702-81b5-8408-00279347f034",
"type": "bot"
}
],
"attempt_number": 1,
"entity": {
"id": "153104cd-477e-8071-b16a-001d9a35ad84",
"type": "comment"
},
"data": {
"page_id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"parent": {
"id": "153104cd-477e-803a-88dc-caececf26478",
"type": "block"
}
}
}
```
# Comment created
Source: https://developers.notion.com/reference/webhooks/comment-created
openapi.json webhook commentCreated
Webhook event for comment created in a workspace
# Comment deleted
Source: https://developers.notion.com/reference/webhooks/comment-deleted
openapi.json webhook commentDeleted
Webhook event for comment deleted in a workspace
# Comment updated
Source: https://developers.notion.com/reference/webhooks/comment-updated
openapi.json webhook commentUpdated
Webhook event for comment updated in a workspace
# Data source content updated
Source: https://developers.notion.com/reference/webhooks/data-source-content-updated
openapi.json webhook dataSourceContentUpdated
Webhook event for data source content updated in a workspace
# Data source created
Source: https://developers.notion.com/reference/webhooks/data-source-created
openapi.json webhook dataSourceCreated
Webhook event for data source created in a workspace
# Data source deleted
Source: https://developers.notion.com/reference/webhooks/data-source-deleted
openapi.json webhook dataSourceDeleted
Webhook event for data source deleted in a workspace
# Data source moved
Source: https://developers.notion.com/reference/webhooks/data-source-moved
openapi.json webhook dataSourceMoved
Webhook event for data source moved in a workspace
# Data source schema updated
Source: https://developers.notion.com/reference/webhooks/data-source-schema-updated
openapi.json webhook dataSourceSchemaUpdated
Webhook event for data source schema updated in a workspace
# Data source undeleted
Source: https://developers.notion.com/reference/webhooks/data-source-undeleted
openapi.json webhook dataSourceUndeleted
Webhook event for data source undeleted in a workspace
# Database content updated
Source: https://developers.notion.com/reference/webhooks/database-content-updated
openapi.json webhook databaseContentUpdated
Webhook event for database content updated in a workspace
# Database created
Source: https://developers.notion.com/reference/webhooks/database-created
openapi.json webhook databaseCreated
Webhook event for database created in a workspace
# Database deleted
Source: https://developers.notion.com/reference/webhooks/database-deleted
openapi.json webhook databaseDeleted
Webhook event for database deleted in a workspace
# Database moved
Source: https://developers.notion.com/reference/webhooks/database-moved
openapi.json webhook databaseMoved
Webhook event for database moved in a workspace
# Database schema updated
Source: https://developers.notion.com/reference/webhooks/database-schema-updated
openapi.json webhook databaseSchemaUpdated
Webhook event for database schema updated in a workspace
# Database undeleted
Source: https://developers.notion.com/reference/webhooks/database-undeleted
openapi.json webhook databaseUndeleted
Webhook event for database undeleted in a workspace
# File upload completed
Source: https://developers.notion.com/reference/webhooks/file-upload-completed
openapi.json webhook fileUploadCompleted
Webhook event for file upload completed in a workspace
# File upload created
Source: https://developers.notion.com/reference/webhooks/file-upload-created
openapi.json webhook fileUploadCreated
Webhook event for file upload created in a workspace
# File upload expired
Source: https://developers.notion.com/reference/webhooks/file-upload-expired
openapi.json webhook fileUploadExpired
Webhook event for file upload expired in a workspace
# File upload failed
Source: https://developers.notion.com/reference/webhooks/file-upload-upload-failed
openapi.json webhook fileUploadUploadFailed
Webhook event for file upload failed in a workspace
# Page content updated
Source: https://developers.notion.com/reference/webhooks/page-content-updated
openapi.json webhook pageContentUpdated
Webhook event for page content updated in a workspace
# Page created
Source: https://developers.notion.com/reference/webhooks/page-created
openapi.json webhook pageCreated
Webhook event for page created in a workspace
# Page deleted
Source: https://developers.notion.com/reference/webhooks/page-deleted
openapi.json webhook pageDeleted
Webhook event for page deleted in a workspace
# Page locked
Source: https://developers.notion.com/reference/webhooks/page-locked
openapi.json webhook pageLocked
Webhook event for page locked in a workspace
# Page moved
Source: https://developers.notion.com/reference/webhooks/page-moved
openapi.json webhook pageMoved
Webhook event for page moved in a workspace
# Page properties updated
Source: https://developers.notion.com/reference/webhooks/page-properties-updated
openapi.json webhook pagePropertiesUpdated
Webhook event for page properties updated in a workspace
# Page transcript deleted
Source: https://developers.notion.com/reference/webhooks/page-transcription-block-transcript-deleted
openapi.json webhook pageTranscriptionBlockTranscriptDeleted
Webhook event for transcript deletion in a page's transcription block
# Page undeleted
Source: https://developers.notion.com/reference/webhooks/page-undeleted
openapi.json webhook pageUndeleted
Webhook event for page undeleted in a workspace
# Page unlocked
Source: https://developers.notion.com/reference/webhooks/page-unlocked
openapi.json webhook pageUnlocked
Webhook event for page unlocked in a workspace
# View created
Source: https://developers.notion.com/reference/webhooks/view-created
openapi.json webhook viewCreated
Webhook event for view created in a workspace
# View deleted
Source: https://developers.notion.com/reference/webhooks/view-deleted
openapi.json webhook viewDeleted
Webhook event for view deleted in a workspace
# View updated
Source: https://developers.notion.com/reference/webhooks/view-updated
openapi.json webhook viewUpdated
Webhook event for view updated in a workspace
# What are Notion Workers?
Source: https://developers.notion.com/workers/get-started/overview
Learn what Notion Workers are, what you can build with them, and how they fit into Notion.
Notion Workers are small Node/TypeScript programs that extend Notion. You write code, deploy it with the [Notion CLI](/cli/get-started/overview), and Notion hosts and runs it for you. No servers to manage.
With Workers, you can:
* **Sync external data** into [Notion databases](/guides/data-apis/working-with-databases) on a schedule.
* **Give Notion AI new tools** that your [Custom Agents](https://www.notion.com/help/custom-agents) can call.
* **Receive webhooks** from external services like GitHub, Stripe, or Zendesk.
Workers are designed to be built with AI coding agents. Scaffold a project, describe what you want, and deploy.
## What you can build
Pull data from Salesforce, Stripe, GitHub, or any API into Notion databases — kept in sync automatically.
Give Notion Custom Agents functions like "create a Jira ticket" or "look up a customer in our CRM."
Receive HTTP events from GitHub pushes, Stripe payments, or any service that sends webhooks.
## How it works
A worker is a single TypeScript file that exports a `Worker` instance. You register **capabilities** on it (syncs, tools, webhooks) and deploy with `ntn workers deploy`:
```typescript src/index.ts theme={null}
import { Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
// Register capabilities on the worker
worker.tool("sayHello", { /* ... */ });
worker.sync("customersSync", { /* ... */ });
worker.webhook("onGithubPush", { /* ... */ });
```
Once deployed, Notion takes over:
* **Syncs** run on a schedule (default every 30 minutes) and write results to Notion databases.
* **Tools** appear in Notion Custom Agents and are called by agents on demand.
* **Webhooks** receive HTTP events from external services and run your handler asynchronously.
Your code runs in a sandboxed Node.js environment. You can make HTTP requests to external APIs, [use secrets](/workers/guides/secrets) stored via the CLI, and authenticate with third-party services through [OAuth](/workers/guides/oauth).
| Concept | What it does |
| :------------- | :------------------------------------------------------------------------------------------------------ |
| **Worker** | The container for your code. One worker per project. |
| **Capability** | Something the worker can do, i.e. a sync, tool, or webhook. A worker can have one or more capabilities. |
| **Database** | A Notion database managed by a sync. You define its schema in code. |
| **Pacer** | Rate-limits outbound API calls so you don't hit third-party quotas. |
| **OAuth** | Handles authorization flows for services like GitHub and Google. |
| **Secrets** | Environment variables stored securely and injected at runtime. |
## Typical workflow
```bash theme={null}
ntn workers new my-worker
cd my-worker
```
This creates a new directory with a `src/index.ts` starter file, TypeScript config, and dependencies.
Add syncs, tools, or webhooks to `src/index.ts`. Use an AI coding agent to help. The template includes prompts and skills, like the `/sync` skill.
```bash theme={null}
ntn workers deploy
```
The CLI bundles your code, uploads it to Notion, and starts running your capabilities.
Edit your code and redeploy.
## Next steps
Create and deploy your first worker in less than five minutes.
Install and configure the Notion CLI.
# Quickstart
Source: https://developers.notion.com/workers/get-started/quickstart
Create and deploy your first worker in minutes.
Get up and running with Workers in a few steps.
## Prerequisites
Version 22 or higher
Version 10 or higher
## Create your first worker
```bash theme={null}
curl -fsSL https://ntn.dev | bash
```
Scaffold a new worker project and enter the directory:
```bash theme={null}
ntn workers new my-worker
cd my-worker
```
Connect to your Notion workspace and deploy:
```bash theme={null}
ntn workers deploy
```
Follow the prompts to authenticate with your Notion workspace.
Execute the included sample tool:
```bash theme={null}
ntn workers exec sayHello -d '{"name": "World"}'
```
## What's in a worker?
The scaffolded project lives in `src/index.ts` and exports a single `Worker` instance:
```typescript src/index.ts theme={null}
import { Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
worker.tool("sayHello", {
title: "Say Hello",
description: "Returns a friendly greeting",
schema: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
additionalProperties: false,
},
execute: ({ name }) => `Hello, ${name}!`,
});
```
## Next steps
Sync external data into Notion databases.
Build custom tools for Notion AI.
Receive HTTP events from external services.
Connect to third-party APIs.
# Using Notion API from a worker
Source: https://developers.notion.com/workers/guides/api-client
Read and write Notion pages and databases from inside a Notion Worker.
Every capability's `execute` function receives a [Notion API client](https://github.com/makenotion/notion-sdk-js) as `context.notion`. This is the official `@notionhq/client` SDK (the same one you'd use outside of Workers):
```typescript theme={null}
worker.tool("example", {
// ...
execute: async (input, { notion }) => {
const page = await notion.pages.retrieve({ page_id: "..." });
return page;
},
});
```
## Authentication
How the client is authenticated depends on how the capability runs:
| Context | How it works |
| :----------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- |
| **Tool called by a Custom Agent** | The platform sets `NOTION_API_TOKEN` automatically. The client has the same permissions as the Custom Agent. No setup needed. |
| **Syncs, webhooks, local testing, `ntn workers exec`** | You must provide a token yourself. |
To set a token for syncs, webhooks, or local development, you have two options:
* **[Personal access token](/guides/get-started/personal-access-tokens):** acts as you and uses your page permissions. You don't need to connect it to each page.
* **[Internal integration token](/guides/get-started/internal-connections):** acts as a bot, with access limited to pages explicitly connected via the Connections menu.
Create a [personal access token](https://www.notion.so/profile/integrations) or an [internal integration](https://www.notion.so/profile/integrations/internal) and copy the token.
```bash theme={null}
ntn workers env set NOTION_API_TOKEN=ntn_...
```
If using an internal integration, open each page or database your worker needs to access, select the **•••** menu in the top right, and add the integration under **Connections**. Personal access tokens don't need this step.
For local development, pull secrets to a `.env` file so the token is available when running `ntn workers exec`:
```bash theme={null}
ntn workers env pull
```
## Common operations
The examples below use `{ notion }` destructured from the second argument to `execute`. They work the same way in tools, syncs, and webhooks:
### Query a database
```typescript theme={null}
const response = await notion.databases.query({
database_id: "...",
filter: {
property: "Status",
select: { equals: "Active" },
},
});
for (const page of response.results) {
// process each page
}
```
### Retrieve a page
```typescript theme={null}
const page = await notion.pages.retrieve({
page_id: "...",
});
```
### Create a page
```typescript theme={null}
await notion.pages.create({
parent: { database_id: "..." },
properties: {
Name: {
title: [{ text: { content: "New item" } }],
},
Status: {
select: { name: "Open" },
},
},
});
```
### Update page properties
```typescript theme={null}
await notion.pages.update({
page_id: "...",
properties: {
Status: {
select: { name: "Done" },
},
},
});
```
### Search
```typescript theme={null}
const results = await notion.search({
query: "meeting notes",
filter: { property: "object", value: "page" },
});
```
### Read page content (blocks)
```typescript theme={null}
const blocks = await notion.blocks.children.list({
block_id: pageId,
});
```
## Permissions
What the client can access depends on the token type:
* **Custom Agent tools:** the client has the same permissions as the Custom Agent.
* **Personal access token:** the client uses your Notion permissions and the token's capabilities.
* **Internal integration token:** the client can only access pages and databases that the integration has been explicitly connected to via the Connections menu.
If a request returns a 403 or 404, check that your token has access to the relevant page.
For full SDK documentation, see the [Notion API reference](/reference/intro) and the [TypeScript SDK on GitHub](https://github.com/makenotion/notion-sdk-js).
## Next steps
Sync external data into Notion databases.
Build custom tools for Notion AI.
Receive HTTP events from external services.
Connect to third-party APIs with user authorization.
Store API keys and credentials securely.
# OAuth
Source: https://developers.notion.com/workers/guides/oauth
Authenticate with third-party APIs like GitHub, Google, and Salesforce from a Notion Worker.
Use OAuth when the API you're connecting to requires user authorization, such as GitHub, Google, Salesforce, and most SaaS APIs. You register an OAuth capability on your worker, deploy, complete the authorization flow via the CLI, and then call `accessToken()` in your code to get a valid token.
## Define an OAuth capability
Call `worker.oauth()` with your provider's OAuth 2.0 endpoints and credentials:
```typescript src/index.ts theme={null}
import { Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
const githubAuth = worker.oauth("githubAuth", {
name: "github-oauth",
authorizationEndpoint: "https://github.com/login/oauth/authorize",
tokenEndpoint: "https://github.com/login/oauth/access_token",
scope: "repo user",
clientId: process.env.GITHUB_CLIENT_ID ?? "",
clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "",
});
```
Store your `clientId` and `clientSecret` as [secrets](/workers/guides/secrets), not in code:
```bash theme={null}
ntn workers env set GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy
```
### Configuration options
| Property | Required | Description |
| :---------------------- | :------- | :---------------------------------------------------------------------------------- |
| `name` | Yes | Unique identifier for this OAuth connection |
| `authorizationEndpoint` | Yes | The provider's OAuth 2.0 authorization URL |
| `tokenEndpoint` | Yes | The provider's OAuth 2.0 token exchange URL |
| `clientId` | Yes | Your OAuth app's client ID |
| `clientSecret` | Yes | Your OAuth app's client secret |
| `scope` | Yes | Space-separated list of OAuth scopes to request |
| `authorizationParams` | No | Additional parameters to include in the authorization request |
| `accessTokenExpireMs` | No | Default token expiry in milliseconds (for providers that don't return `expires_in`) |
## Deploy and authorize
Setting up OAuth requires multiple steps in a specific order. The worker must be deployed before you can store secrets or start the OAuth flow:
If you haven't deployed at least once already, do so first. The first deploy registers the worker with Notion. Your OAuth credentials won't be available yet (that's expected).
```bash theme={null}
ntn workers deploy
```
```bash theme={null}
ntn workers oauth show-redirect-url
```
You'll need this when creating the OAuth app with your provider.
Go to your provider's developer settings (e.g., GitHub Developer Settings, Google Cloud Console) and create an OAuth app. Add the redirect URL from the previous step as an authorized redirect URI. Copy the client ID and client secret.
```bash theme={null}
ntn workers env set GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy
```
Redeploy so the worker picks up the credentials:
```bash theme={null}
ntn workers deploy
```
```bash theme={null}
ntn workers oauth start githubAuth
```
This opens a browser window where you authorize the connection. Once complete, the worker runtime stores the token securely.
## Use the token
Call `accessToken()` on the OAuth capability object to get a valid token. The runtime handles refresh automatically. This works in [tools](/workers/guides/tools), [syncs](/workers/guides/syncs), and [webhooks](/workers/guides/webhooks):
```typescript theme={null}
import { j } from "@notionhq/workers/schema-builder";
worker.tool("getGitHubRepos", {
title: "Get GitHub repos",
description: "Fetch the user's GitHub repositories",
schema: j.object({}),
execute: async () => {
const token = await githubAuth.accessToken();
const response = await fetch("https://api.github.com/user/repos", {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
},
});
```
This works the same way in syncs and webhooks:
```typescript theme={null}
import * as Builder from "@notionhq/workers/builder";
worker.sync("githubIssuesSync", {
database: issues,
execute: async (state) => {
const token = await githubAuth.accessToken();
const response = await fetch("https://api.github.com/issues", {
headers: { Authorization: `Bearer ${token}` },
});
const items = await response.json();
return {
changes: items.map((issue) => ({
type: "upsert" as const,
key: String(issue.id),
properties: {
Title: Builder.title(issue.title),
"Issue ID": Builder.richText(String(issue.id)),
},
})),
hasMore: false,
};
},
});
```
## Test locally
Once you've completed the OAuth flow (via `ntn workers oauth start`), pull your environment to get a fresh access token locally:
```bash theme={null}
ntn workers env pull
```
This writes a `.env` file with all your worker's secrets, including a fresh OAuth access token. The server refreshes the token automatically before returning it, so the token you get is always valid.
You can then test your capability locally with the `--local` flag:
```bash theme={null}
ntn workers exec getGitHubRepos --local
```
OAuth tokens expire. If you get a 401 from the provider, run `ntn workers env pull` again to get a refreshed token.
## Examples
### Google
```typescript theme={null}
const googleAuth = worker.oauth("googleAuth", {
name: "google-oauth",
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
tokenEndpoint: "https://oauth2.googleapis.com/token",
scope: "https://www.googleapis.com/auth/calendar.readonly",
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
authorizationParams: {
access_type: "offline",
prompt: "consent",
},
});
```
### Salesforce
```typescript theme={null}
const salesforceAuth = worker.oauth("salesforceAuth", {
name: "salesforce-oauth",
authorizationEndpoint: "https://login.salesforce.com/services/oauth2/authorize",
tokenEndpoint: "https://login.salesforce.com/services/oauth2/token",
scope: "api refresh_token",
clientId: process.env.SALESFORCE_CLIENT_ID ?? "",
clientSecret: process.env.SALESFORCE_CLIENT_SECRET ?? "",
});
```
Some providers (like Salesforce) don't return `expires_in` in their token response. Set `accessTokenExpireMs` to a sensible default (e.g., `3600000` for 1 hour) so the runtime knows when to refresh.
## Next steps
Store API keys and OAuth credentials securely.
Sync external data into Notion databases.
Receive HTTP events from external services.
# How to manage secrets
Source: https://developers.notion.com/workers/guides/secrets
Store API keys and credentials for your worker securely.
Secrets let your [Notion Workers](/workers/get-started/overview) use API keys, tokens, client secrets, webhook signing secrets, and other credentials without committing those values to your source code.
Notion encrypts worker secrets at rest and exposes them as environment variables at runtime. In your worker code, read them from `process.env`.
```typescript theme={null}
const apiKey = process.env.OPENWEATHER_API_KEY;
```
Never commit `.env` files or any type of secret to source control. Worker
projects created from the template include `.env` and `.env.*` in `.gitignore`
by default.
## Add a secret
Use `ntn workers env set` to store one or more secrets for your worker:
```bash theme={null}
ntn workers env set OPENWEATHER_API_KEY=your-secret
```
To set multiple secrets at once, pass multiple `KEY=value` pairs:
```bash theme={null}
ntn workers env set GITHUB_CLIENT_ID=your-client-id GITHUB_CLIENT_SECRET=your-client-secret
```
If a key already exists, setting it again replaces the previous value.
Quote values that contain spaces, shell metacharacters, or other characters
your shell might interpret.
## Use a secret in worker code
Read secrets from `process.env` inside your worker capability:
```typescript theme={null}
worker.tool("getWeather", {
title: "Get Weather",
description: "Fetch the current weather for a city",
schema: {
type: "object",
properties: {
city: { type: "string" },
},
required: ["city"],
additionalProperties: false,
},
execute: async ({ city }) => {
const apiKey = process.env.OPENWEATHER_API_KEY;
if (!apiKey) {
throw new Error("OPENWEATHER_API_KEY is not configured");
}
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(
city,
)}&appid=${apiKey}`,
);
return response.json();
},
});
```
## Pull secrets for local development
When you run a worker locally, use a `.env` file to provide the same environment variables that the hosted worker receives.
Pull remote secrets into `.env`:
```bash theme={null}
ntn workers env pull
```
Or write to a different file:
```bash theme={null}
ntn workers env pull --file=.env.local
```
If the file already exists, `pull` preserves comments, blank lines, and local-only keys. It updates keys that also exist remotely, then appends new remote keys.
For non-interactive scripts, add `--yes` to skip the confirmation prompt:
```bash theme={null}
ntn workers env pull --yes
```
Treat any pulled `.env` file as sensitive. Confirm that the file is ignored by Git
before you pull secrets into a project.
## Push local secrets to your worker
If you've added secrets locally to `.env`, push them to the hosted worker:
```bash theme={null}
ntn workers env push
```
Or push a different file:
```bash theme={null}
ntn workers env push --file=.env.local
```
`push` adds new local keys and updates changed local keys. It does not remove keys that exist only in the remote worker environment.
For non-interactive scripts, add `--yes`:
```bash theme={null}
ntn workers env push --yes
```
## Manage another worker
When you run these commands inside a worker project, the CLI reads the worker ID from `workers.json`. To manage a different worker, pass its worker ID.
For `set` and `unset`, use `--worker-id`:
```bash theme={null}
ntn workers env set --worker-id API_KEY=your-secret
ntn workers env unset --worker-id API_KEY
```
For `list`, `pull`, and `push`, pass the worker ID as the positional argument:
```bash theme={null}
ntn workers env list
ntn workers env pull
ntn workers env push
```
## Use secrets for OAuth client credentials
For custom OAuth providers, store the OAuth client ID and client secret as worker secrets:
```bash theme={null}
ntn workers env set GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy
```
Then read them from `process.env` in the OAuth capability configuration:
```typescript theme={null}
worker.oauth("githubAuth", {
name: "github-oauth",
authorizationEndpoint: "https://github.com/login/oauth/authorize",
tokenEndpoint: "https://github.com/login/oauth/access_token",
scope: "repo user",
clientId: process.env.GITHUB_CLIENT_ID ?? "",
clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "",
});
```
## Command summary
| Command | What it does |
| :------------------------------ | :------------------------------------------------------ |
| `ntn workers env set KEY=value` | Stores or replaces one or more secrets |
| `ntn workers env list` | Lists secret keys without revealing values |
| `ntn workers env unset KEY` | Removes a secret |
| `ntn workers env pull` | Writes remote secrets to a local `.env` file |
| `ntn workers env push` | Adds or updates remote secrets from a local `.env` file |
See the [CLI command reference](/cli/reference/commands) for all `ntn workers env` flags and options.
## Next steps
Authenticate with third-party APIs using OAuth.
Sync external data into Notion databases.
Build custom tools for Notion AI.
Receive HTTP events from external services.
# Syncs
Source: https://developers.notion.com/workers/guides/syncs
Pull external data into Notion databases and keep it up to date.
A sync pulls data from external sources like Salesforce, Stripe, and GitHub and writes it to a [Notion database](/guides/data-apis/working-with-databases). You define a schema for the database and an `execute` function that returns the data. Notion runs it on a schedule and manages the database for you.
## Define a database and sync
Every sync needs a database to write to. Declare one with `worker.database()`, then register a sync that targets it:
```typescript src/index.ts theme={null}
import { Worker } from "@notionhq/workers";
import * as Builder from "@notionhq/workers/builder";
import * as Schema from "@notionhq/workers/schema";
const worker = new Worker();
export default worker;
const issues = worker.database("issues", {
// only "managed" type is supported for now
type: "managed",
// the initial title of the database
initialTitle: "Issues",
// the property that uniquely identifies each row
primaryKeyProperty: "Issue ID",
// the schema defines the structure of the database
schema: {
// define each database property and its type
properties: {
Name: Schema.title(),
"Issue ID": Schema.richText(),
Status: Schema.richText(),
},
},
});
worker.sync("issuesSync", {
// ...
});
```
`primaryKeyProperty` tells Notion which property uniquely identifies each row. This is typically the entity's ID in the external API (e.g., a Salesforce Contact ID or GitHub issue ID). When your sync emits a record with the same `key`, Notion updates the existing row instead of creating a duplicate.
Syncs currently create and manage their own databases. Support for syncing to existing databases is coming soon.
### Schema and properties
The `schema.properties` object defines the columns of your Notion database. Each property uses a `Schema` helper to declare its type, and each upsert uses the corresponding `Builder` helper to set its value.
For the full list of supported property types, see [Schema and builders](/workers/reference/schema).
## Set page icons and covers
Each upsert can optionally set page metadata with `icon` and `cover`. Use `Builder.imageCover()` with an external image URL and an optional position from `0` (top) to `1` (bottom):
```typescript theme={null}
worker.sync("tasksSync", {
database: tasks,
execute: async () => ({
changes: [
{
type: "upsert" as const,
key: "task-123",
properties: {
Name: Builder.title("Write docs"),
"Task ID": Builder.richText("task-123"),
},
icon: Builder.notionIcon("document", "blue"),
cover: Builder.imageCover("https://example.com/cover.jpg", 0.5),
},
],
hasMore: false,
}),
});
```
## Choose a sync mode
Workers support two sync modes. Pick the one that fits your needs:
Each sync cycle returns the **full dataset**. After the final `hasMore: false`, any rows not seen during that cycle are automatically deleted.
Best for smaller datasets (under 10k records) or APIs that don't support change tracking. Also used as the [backfill half](#combine-backfill-and-delta-syncs) of a backfill + delta pair.
```typescript theme={null}
worker.sync("teamsSync", {
database: teams,
mode: "replace",
execute: async (state) => {
const page = state?.page ?? 1;
const { items, hasMore } = await fetchPage(page, 100);
return {
changes: items.map((item) => ({
type: "upsert" as const,
key: item.id,
properties: {
Name: Builder.title(item.name),
ID: Builder.richText(item.id),
},
})),
hasMore,
nextState: hasMore ? { page: page + 1 } : undefined,
};
},
});
```
Each sync cycle returns only **changes since the last run**. Rows not mentioned are left as-is. Deletions must be explicit.
Best for large datasets (10k+ records) or APIs that provide a changes endpoint or cursor. Typically used as the [delta half](#combine-backfill-and-delta-syncs) of a backfill + delta pair.
```typescript theme={null}
worker.sync("eventsSync", {
database: events,
mode: "incremental",
execute: async (state) => {
const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
return {
changes: [
...upserts.map((item) => ({
type: "upsert" as const,
key: item.id,
properties: {
Name: Builder.title(item.name),
ID: Builder.richText(item.id),
},
})),
...deletes.map((id) => ({
type: "delete" as const,
key: id,
})),
],
hasMore: Boolean(nextCursor),
nextState: nextCursor ? { cursor: nextCursor } : undefined,
};
},
});
```
## Paginate large datasets
When syncing more than a few hundred records, break the work into batches. The runtime calls `execute` repeatedly until you return `hasMore: false`:
1. Return a batch of changes with `hasMore: true` and a `nextState` value.
2. The runtime calls `execute` again, passing that state back as the first argument.
3. Repeat until you return `hasMore: false`.
`nextState` can be any serializable value, such as a cursor string, page number, timestamp, or object. Start with batch sizes of \~100 records.
```typescript theme={null}
worker.sync("paginatedSync", {
database: records,
execute: async (state) => {
const { items, nextCursor } = await fetchPage(state?.cursor);
return {
changes: items.map((item) => ({
type: "upsert" as const,
key: item.id,
properties: {
Name: Builder.title(item.name),
ID: Builder.richText(item.id),
},
})),
hasMore: Boolean(nextCursor),
nextState: nextCursor ? { cursor: nextCursor } : undefined,
};
},
});
```
## Set a schedule
A schedule controls how often Notion triggers your sync. Each time it triggers, the runtime calls `execute` repeatedly until it returns `hasMore: false`, then waits for the next scheduled trigger. The default schedule is every 30 minutes.
```typescript theme={null}
worker.sync("frequentSync", {
database: myDb,
schedule: "5m",
// ...
});
```
| Value | Behavior |
| :------------------------------ | :---------------------------------- |
| `"5m"`, `"15m"`, `"1h"`, `"1d"` | Run at the given interval |
| `"manual"` | Only run when triggered via the CLI |
Minimum schedule is `"5m"`, maximum is `"7d"`.
## Combine backfill and delta syncs
A single replace sync works for small datasets, but most real integrations need two things: fast updates (minutes, not hours) and the ability to re-sync everything when needed. You get both by registering two syncs against the same database:
* A **delta sync** runs on a schedule and fetches only what changed since the last run. This keeps the database near-real-time.
* A **backfill sync** paginates the entire upstream dataset. You trigger it manually, for example after a schema change, to populate a new property, or to catch anything the delta missed.
Since both syncs share a database and key space, upserts from both operate on the same rows. The delta keeps the database current and the backfill re-syncs the full dataset when you need to:
| | Delta sync | Backfill sync |
| :--------------- | :-------------------------------------------------------- | :------------------------------------- |
| **Mode** | `incremental` | `replace` |
| **Schedule** | `"5m"` or `"30m"` | `"manual"` |
| **What it does** | Grabs recent changes via `updated_since` or a change feed | Paginates the entire upstream dataset |
| **Deletes** | Emits `type: "delete"` if the API supports it | Mark-and-sweep catches everything else |
| **When it runs** | Continuously on schedule | On demand |
```typescript theme={null}
// Delta: near-real-time updates
worker.sync("ticketsDelta", {
database: tickets,
mode: "incremental",
schedule: "5m",
execute: async (state) => {
await apiPacer.wait();
const { items, nextCursor } = await fetchTicketChanges(state?.cursor);
return {
changes: items.map((t) => ({
type: "upsert" as const,
key: t.id,
properties: {
Summary: Builder.title(t.summary),
"Ticket ID": Builder.richText(t.id),
},
})),
hasMore: Boolean(nextCursor),
nextState: nextCursor ? { cursor: nextCursor } : undefined,
};
},
});
// Backfill: full dataset sweep, run manually
worker.sync("ticketsBackfill", {
database: tickets,
mode: "replace",
schedule: "manual",
execute: async (state) => {
const page = state?.page ?? 1;
await apiPacer.wait();
const { items, hasMore } = await fetchAllTickets(page);
return {
changes: items.map((t) => ({
type: "upsert" as const,
key: t.id,
properties: {
Summary: Builder.title(t.summary),
"Ticket ID": Builder.richText(t.id),
},
})),
hasMore,
nextState: hasMore ? { page: page + 1 } : undefined,
};
},
});
```
In this example, to run a backfill at any point in the future, you'd reset the state then trigger the sync to start running:
```bash theme={null}
ntn workers sync state reset ticketsBackfill
ntn workers sync trigger ticketsBackfill
```
This pattern gives you operational flexibility: run a backfill after a schema change to populate a new property, or after a bug fix to correct drifted data. This pattern also handles deletes cleanly even when the API doesn't surface them, as the backfill's replace-mode mark-and-sweep catches anything the delta missed.
If both syncs hit the same API, give them the same [pacer](#rate-limit-outbound-requests). The runtime automatically splits the rate limit budget between them.
## Relate two databases
Link databases together with `Schema.relation()` and `Builder.relation()`:
```typescript theme={null}
const projects = worker.database("projects", {
type: "managed",
initialTitle: "Projects",
primaryKeyProperty: "Project ID",
schema: {
properties: {
Name: Schema.title(),
"Project ID": Schema.richText(),
},
},
});
const tasks = worker.database("tasks", {
type: "managed",
initialTitle: "Tasks",
primaryKeyProperty: "Task ID",
schema: {
properties: {
Name: Schema.title(),
"Task ID": Schema.richText(),
Project: Schema.relation("projects", {
twoWay: true,
relatedPropertyName: "Tasks",
}),
},
},
});
worker.sync("projectsSync", {
database: projects,
execute: async () => { /* ... */ },
});
worker.sync("tasksSync", {
database: tasks,
execute: async () => {
const items = await fetchTasks();
return {
changes: items.map((task) => ({
type: "upsert" as const,
key: task.id,
properties: {
Name: Builder.title(task.name),
"Task ID": Builder.richText(task.id),
Project: [Builder.relation(task.projectId)],
},
})),
hasMore: false,
};
},
});
```
In the example above, `Schema.relation("projects")` references the database name `projects` from `worker.database("projects", ...)`, and the `twoWay: true` option adds a "Tasks" rollup column to the Projects database automatically.
## Authenticate with external APIs
Most syncs need credentials for the external API they pull from. You have two options:
* **API keys and tokens:** store them as [secrets](/workers/guides/secrets) and read from `process.env`.
* **OAuth:** for APIs that require user authorization (GitHub, Google, Salesforce), register an [OAuth capability](/workers/guides/oauth) and call `accessToken()` in your `execute` function.
To call the Notion API from a sync (e.g., to read pages or update properties beyond sync changes), see [Using Notion API from a worker](/workers/guides/api-client).
## Rate-limit outbound requests
Use a pacer to avoid hitting third-party API rate limits:
```typescript theme={null}
const api = worker.pacer("api", { allowedRequests: 10, intervalMs: 1000 });
worker.sync("customersSync", {
database: customers,
execute: async (state) => {
await api.wait();
const data = await fetchCustomers(state?.cursor);
// ...
},
});
```
`await api.wait()` blocks until a request slot is available. In this example, at most 10 requests per second.
## Manage syncs from the CLI
```bash theme={null}
# Live-updating status dashboard
ntn workers sync status
# Preview output without writing to the database
ntn workers sync trigger --preview
# Trigger a real sync immediately
ntn workers sync trigger
# Reset sync state (restart from scratch)
ntn workers sync state reset
# Pause a sync
ntn workers capabilities disable
# Resume a sync
ntn workers capabilities enable
```
Deploying does **not** reset sync state. Syncs resume from their last cursor position. See [Resetting and migrating state](#reset-and-migrate-state) below.
## Reset and migrate state
Deploys never clear sync state. Your sync picks up where it left off. If you need to start fresh (e.g., after changing your schema or fixing a bug in your `execute` function), reset the state:
```bash theme={null}
ntn workers sync state reset
```
This clears the stored `nextState` so the next run starts from scratch, as if the sync had never run before.
To inspect the current state before deciding whether to reset:
```bash theme={null}
ntn workers sync state get
```
## Troubleshooting syncs
### Sync runs but no rows appear
* Check `ntn workers sync trigger --preview` to see what your `execute` function returns without writing to the database. If the preview is empty, the issue is in your data-fetching code.
* Make sure the `key` in each change matches the property named by `primaryKeyProperty`.
### Rows are duplicated
* Each row needs a unique `key`. If two changes share the same key, the second overwrites the first. If keys differ, Notion creates separate rows. Double-check that your key is the stable external ID, not a value that changes between runs.
### Stale rows aren't deleted (replace mode)
* Replace mode only deletes stale rows after the final page returns `hasMore: false`. If your sync errors partway through, no deletions happen (this is intentional to avoid data loss).
### Sync is stuck or out of date
* Run `ntn workers sync status` to see the current state and last run time.
* If state is corrupted or outdated, reset it with `ntn workers sync state reset `.
### Checking logs
List recent runs:
```bash theme={null}
ntn workers runs list
```
View execution logs for a specific run:
```bash theme={null}
ntn workers runs logs
```
See the [CLI command reference](/cli/reference/commands) for the full list of `ntn workers sync` flags and options.
## Next steps
Full reference for database property types and value builders.
Detailed API docs for worker.sync(), worker.database(), and worker.pacer().
Store API keys and credentials for your sync.
Connect to APIs that require user authorization.
# How to write an agent tool
Source: https://developers.notion.com/workers/guides/tools
Build custom tools that Notion Custom Agents can call.
Agent tools are functions that [Notion Custom Agents](https://www.notion.com/help/custom-agents) can call. Use a tool when an agent needs to look up external data, call your own service, perform an action that is not built into Notion or available through MCP, or apply custom validation that an MCP server does not provide.
This guide shows you how to add a tool to a worker, define its inputs, test it locally, and deploy it.
## Add a tool
In `src/index.ts`, import `Worker` and the schema builder:
```typescript theme={null}
import { Worker } from "@notionhq/workers";
import { j } from "@notionhq/workers/schema-builder";
const worker = new Worker();
export default worker;
```
Register a tool with `worker.tool`:
```typescript theme={null}
worker.tool("lookupCustomer", {
title: "Lookup Customer",
description: "Find a customer by email address.",
schema: j.object({
email: j.email().describe("The customer's email address."),
}),
hints: { readOnlyHint: true },
execute: async ({ email }) => {
const customer = await findCustomerByEmail(email);
if (!customer) {
return {
found: false,
message: `No customer found for ${email}.`,
};
}
return {
found: true,
name: customer.name,
plan: customer.plan,
accountUrl: customer.accountUrl,
};
},
});
```
The first argument, `"lookupCustomer"`, is the tool key. Use it when you run the tool from the CLI.
Choose a key that is stable and specific. If you rename a tool key, existing agent configuration that refers to the old key needs to be updated.
## Describe when the agent should use it
The `title` and `description` help Notion Custom Agents and users understand the tool. Keep the title short and write the description as an instruction boundary: what the tool does, and when it should be used.
```typescript theme={null}
worker.tool("createSupportTicket", {
title: "Create Support Ticket",
description:
"Create a support ticket when the user asks to escalate an issue to the support team.",
// ...
});
```
Avoid descriptions that are too broad, such as "Run support operations". A narrow description makes the tool easier for the agent to choose correctly.
## Define the input schema
Use the `j` schema builder to define the values your tool accepts. The builder creates a JSON Schema and gives TypeScript types to the `execute` input. See the [Schema and builders reference](/workers/reference/schema) for all available types.
```typescript theme={null}
worker.tool("searchTickets", {
// ...
schema: j.object({
query: j.string().describe("The search query."),
limit: j
.number()
.describe("The maximum number of results to return.")
.nullable(),
status: j.enum("open", "closed").describe("The ticket status to search."),
}),
// ...
});
```
Use `.describe()` on every field. Field descriptions tell the agent what each value means.
Use `.nullable()` for optional fields:
```typescript theme={null}
worker.tool("searchTickets", {
// ...
schema: j.object({
query: j.string().describe("The search query."),
limit: j
.number()
.describe("The maximum number of results to return.")
.nullable(),
}),
// ...
});
```
In `execute`, handle nullable fields explicitly:
```typescript theme={null}
worker.tool("searchTickets", {
// ...
execute: async ({ query, limit }) => {
const results = await searchTickets({
query,
limit: limit ?? 10,
});
return { results };
},
});
```
The schema builder marks object properties as required and sets `additionalProperties: false`. Use `.nullable()` instead of omitting a property from the schema when a value is optional.
## Return structured output
A tool can return a string or any JSON-serialisable value. Prefer structured objects for results the agent may need to inspect or reuse:
```typescript theme={null}
worker.tool("lookupCustomer", {
// ...
execute: async ({ email }) => {
const customer = await findCustomerByEmail(email);
if (!customer) {
return { found: false };
}
return {
found: true,
customer: {
name: customer.name,
plan: customer.plan,
accountUrl: customer.accountUrl,
},
};
},
});
```
If the output has a predictable shape, add `outputSchema`. The worker validates the returned value against this schema.
```typescript theme={null}
worker.tool("searchTickets", {
// ...
outputSchema: j.object({
results: j.array(
j.object({
id: j.string().describe("The ticket ID."),
title: j.string().describe("The ticket title."),
url: j.string().describe("A URL for the ticket."),
}),
),
}),
execute: async ({ query }) => {
const tickets = await searchTickets(query);
return {
results: tickets.map(ticket => ({
id: ticket.id,
title: ticket.title,
url: ticket.url,
})),
};
},
});
```
## Mark read-only tools
If a tool only reads data and has no side effects, set `readOnlyHint`:
```typescript theme={null}
worker.tool("previewAccountDeletion", {
title: "Preview Account Deletion",
description:
"Inspect what would be affected if an account were deleted, without deleting or changing anything.",
// ...
hints: { readOnlyHint: true },
// ...
});
```
Read-only tools are safe to call repeatedly and can be auto-executed under the default policy. Tools without this hint are treated as write tools, so the Custom Agent will ask for permission from the user before executing the tool unless the agent's settings change that behaviour.
## Use Notion and external APIs
The second argument to `execute` is a context object. Use `context.notion` to call the Notion API with the worker's authenticated Notion client. The client has the same permissions as the Custom Agent running the tool:
```typescript theme={null}
worker.tool("getPageTitle", {
title: "Get Page Title",
description: "Read the title of a Notion page.",
schema: j.object({
pageId: j.string().describe("The Notion page ID."),
}),
hints: { readOnlyHint: true },
execute: async ({ pageId }, { notion }) => {
const page = await notion.pages.retrieve({ page_id: pageId });
return page;
},
});
```
For external APIs, store credentials as worker [secrets](/workers/guides/secrets) and read them from `process.env`. For APIs that require user authorization (GitHub, Google, Salesforce), use [OAuth](/workers/guides/oauth) instead.
For more on `context.notion`, see [Using Notion API from a worker](/workers/guides/api-client).
## Test a tool locally
Run the tool from your worker project with `ntn workers exec --local`:
```bash theme={null}
ntn workers exec lookupCustomer --local -d '{"email":"ada@example.com"}'
```
The CLI loads `.env` by default for local execution. To load another file, pass `--dotenv`:
```bash theme={null}
ntn workers exec lookupCustomer --local --dotenv .env.local -d '{"email":"ada@example.com"}'
```
The Workers runtime injects a preauthenticated Notion SDK client when your
tool runs in Notion, but that client is not available during local execution.
When testing locally, we recommend setting `NOTION_API_TOKEN` to a
[personal access token](/guides/get-started/personal-access-tokens) in your
`.env` file, which the SDK uses to create the client.
If you do not want to load a `.env` file, pass `--no-dotenv`:
```bash theme={null}
ntn workers exec lookupCustomer --local --no-dotenv -d '{"email":"ada@example.com"}'
```
Use local execution to check schema validation, returned output, and errors before deploying.
## Deploy and run the tool
Deploy the worker:
```bash theme={null}
ntn workers deploy
```
After deployment, run the hosted tool from the CLI:
```bash theme={null}
ntn workers exec lookupCustomer -d '{"email":"ada@example.com"}'
```
When the tool works as expected, add it to a Notion Custom Agent from the agent's tool configuration.
See the [CLI command reference](/cli/reference/commands) for all `ntn workers exec` flags and options.
## Next steps
Full reference for the j schema builder and all input types.
Detailed API docs for worker.tool(), hints, and output schemas.
Store API keys and credentials for your tools.
Read and write Notion data from inside a tool.
# Webhooks
Source: https://developers.notion.com/workers/guides/webhooks
Receive HTTP events from external services in a Notion Worker.
Webhooks expose an HTTP endpoint that external services can call. Use them to push events from external systems into Notion, such as a GitHub push, Stripe event, Zendesk ticket update, or any service that can send an HTTP webhook.
## Basic webhook
Define a webhook capability on your worker like this:
```typescript theme={null}
import { Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
worker.webhook("onExternalEvent", {
title: "External Event Handler",
description: "Processes incoming webhook requests",
execute: async (events) => {
for (const event of events) {
console.log("Delivery:", event.deliveryId);
console.log("Method:", event.method);
console.log("Body:", event.body);
}
},
});
```
After you deploy, Notion creates a URL for each webhook capability. Give that URL to the external service as its webhook destination:
```bash theme={null}
ntn workers deploy
ntn workers webhooks list
```
## The event object
The `execute` function receives an array of `WebhookEvent` objects. The array currently contains one event, but may contain multiple events in the future.
| Property | Type | Description |
| :----------- | :------------------------ | :-------------------------------------------------------------------------------------------- |
| `deliveryId` | `string` | Unique ID for this Notion delivery. It is stable across retries for the same inbound request. |
| `body` | `Record` | Parsed JSON body. If the request body is not a JSON object, this is `{}`. |
| `rawBody` | `string` | Original request body as a string. Use this for signature verification. |
| `headers` | `Record` | Request headers. Header names are lowercased. |
| `method` | `string` | HTTP method used by the sender. Webhook URLs accept `POST` requests. |
Use the external provider's own event ID for idempotency when the payload includes one.
`deliveryId` is useful when Notion retries running your worker, but a provider may
redeliver the same event as a new HTTP request.
## Webhook URLs
Webhook URLs include a unique ID that acts as a shared secret:
```text theme={null}
https://www.notion.com/webhooks/worker/{spaceId}/{workerId}/{uniqueWebhookId}/{webhookName}
```
Use the CLI to print the URLs for a deployed worker:
```bash theme={null}
ntn workers webhooks list
```
For scripts, use JSON or tab-separated output:
```bash theme={null}
ntn workers webhooks list --json
ntn workers webhooks list --plain
```
Treat webhook URLs as secrets. Anyone with the full URL can send events to the
webhook endpoint unless you add provider-specific signature verification inside your worker.
## Verify requests
Most webhook providers can sign requests with a shared [secret](/workers/guides/secrets). Store the signing secret as a worker secret, verify each request using `event.rawBody` and `event.headers`, and throw `WebhookVerificationError` when verification fails:
```typescript theme={null}
import * as crypto from "node:crypto";
import { WebhookVerificationError, Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
/**
* Verify a GitHub webhook signature.
* GitHub sends the HMAC-SHA256 signature in the X-Hub-Signature-256 header
* as "sha256={hex}". The raw body must be used for verification.
*/
function verifyGitHubSignature(
rawBody: string,
headers: Record,
): void {
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new WebhookVerificationError("GITHUB_WEBHOOK_SECRET not configured");
}
const signature = headers["x-hub-signature-256"];
if (!signature?.startsWith("sha256=")) {
throw new WebhookVerificationError("Invalid GitHub signature");
}
const expected = `sha256=${crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex")}`;
if (signature.length !== expected.length) {
throw new WebhookVerificationError("Invalid GitHub signature");
}
// Use timing-safe comparison to prevent timing attacks.
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new WebhookVerificationError("Invalid GitHub signature");
}
}
worker.webhook("onGithubPush", {
title: "GitHub Push Webhook",
description: "Handles push events from GitHub repositories",
execute: async (events) => {
for (const event of events) {
verifyGitHubSignature(event.rawBody, event.headers);
console.log("Verified GitHub event:", event.body);
}
},
});
```
Set the secret before deploying or push it from your local `.env` file:
```bash theme={null}
ntn workers env set GITHUB_WEBHOOK_SECRET=your-secret
```
See [Secrets](/workers/guides/secrets) for more ways to manage worker environment variables.
After 5 consecutive `WebhookVerificationError` failures, Notion blocks that
webhook before running your handler. Redeploy the worker to reset the failure
counter.
## Execution and retries
When a webhook request reaches Notion, Notion validates the URL, enqueues the event, and responds with `202 Accepted`. Your worker runs asynchronously after the HTTP response is sent.
If your handler throws `WebhookVerificationError`, Notion records a verification failure and does not retry that event. If your handler throws another error, Notion retries the worker run up to 3 times.
Successful runs reset the consecutive verification failure counter.
## Use Notion from a webhook
Webhook handlers receive the same context object as other capabilities, including `context.notion`, the Notion API SDK client:
```typescript theme={null}
worker.webhook("createPageFromWebhook", {
title: "Create Page From Webhook",
description: "Creates a page when an external event is received",
execute: async (events, { notion }) => {
const databaseId = process.env.MY_WEBHOOK_DATABASE_ID;
if (!databaseId) {
throw new Error("MY_WEBHOOK_DATABASE_ID is not configured");
}
for (const event of events) {
const externalId =
typeof event.body.id === "string" ? event.body.id : event.deliveryId;
await notion.pages.create({
parent: { database_id: databaseId },
properties: {
Name: {
title: [
{
text: {
content: `Webhook event ${externalId}`,
},
},
],
},
},
});
}
},
});
```
For webhooks, `context.notion` is not automatically authenticated. To call the Notion API, create an internal integration, give it access to the relevant pages or databases, and store the integration token in `NOTION_API_TOKEN`:
```bash theme={null}
ntn workers env set NOTION_API_TOKEN=secret_xxx
```
At runtime, `context.notion` reads `process.env.NOTION_API_TOKEN` and uses it as the Notion API client token.
For more information about creating an integration token for a worker, see [Using Notion API from a worker](/workers/guides/api-client).
## Inspect runs
Use worker run logs to debug webhook executions:
```bash theme={null}
ntn workers runs list
ntn workers runs logs
```
To find recent webhook runs quickly:
```bash theme={null}
ntn workers runs list --plain | grep webhook
```
See the [CLI command reference](/cli/reference/commands) for all `ntn workers` flags and options.
## Next steps
Store webhook signing secrets and API keys.
Read and write Notion data from a webhook handler.
Authenticate with third-party APIs from your webhook.
Detailed API docs for worker.webhook() and WebhookVerificationError.
# Schema and builders
Source: https://developers.notion.com/workers/reference/schema
Define database schemas, database property values, and tool schemas.
The Workers SDK includes three schema-related exports:
```typescript theme={null}
import { j } from "@notionhq/workers/schema-builder";
import * as Schema from "@notionhq/workers/schema";
import * as Builder from "@notionhq/workers/builder";
```
| Import | Used by | Description |
| --------- | ------------------------------ | ------------------------------------------------------ |
| `j` | [Tools](/workers/guides/tools) | JSON Schema builder for tool input and output schemas. |
| `Schema` | [Syncs](/workers/guides/syncs) | Database property schema helpers for sync databases. |
| `Builder` | [Syncs](/workers/guides/syncs) | Database property value helpers for sync changes. |
## Schema builder
The `j` export from `@notionhq/workers/schema-builder` builds JSON Schemas
for [tool](/workers/reference/sdk#worker-tool) input and output. Use it instead
of hand-writing JSON Schema so tool contracts stay compatible across a wide
range of closed and open-source models. Builder methods return a
`SchemaBuilder`.
```typescript theme={null}
interface SchemaBuilder {
describe(text: string): SchemaBuilder;
nullable(): SchemaBuilder;
}
```
### j.object()
```typescript theme={null}
j.object
>>(
properties: P,
): SchemaBuilder<{ [K in keyof P]: Infer
}>
```
Creates an object schema. All provided properties are included in `required`, and `additionalProperties` is set to `false`.
```typescript theme={null}
j.object({
query: j.string(),
limit: j.number().nullable(),
})
```
### j.string()
```typescript theme={null}
j.string(): SchemaBuilder
```
Creates a string schema.
```typescript theme={null}
j.string().describe("Search query.")
```
### j.number()
```typescript theme={null}
j.number(): SchemaBuilder
```
Creates a number schema.
```typescript theme={null}
j.number().describe("Maximum number of results.")
```
### j.array()
```typescript theme={null}
j.array(
items: SchemaBuilder,
options?: { minItems?: 0 | 1 },
): SchemaBuilder
```
Creates an array schema.
```typescript theme={null}
j.array(j.string())
j.array(j.string(), { minItems: 1 })
```
### .describe()
```typescript theme={null}
schema.describe(text: string): SchemaBuilder
```
Sets the JSON Schema `description` field and returns a new builder.
```typescript theme={null}
j.string().describe("Email address for the assignee.")
```
### .nullable()
```typescript theme={null}
schema.nullable(): SchemaBuilder
```
Wraps the schema in `anyOf` with `{ type: "null" }` and returns a new builder. In object schemas, the property is still required, but its value may be `null`.
```typescript theme={null}
j.object({
dueDate: j.string().nullable().describe("ISO date, or null when unset."),
})
```
### j.integer()
```typescript theme={null}
j.integer(): SchemaBuilder
```
Creates an integer schema.
```typescript theme={null}
j.integer().describe("Whole number of retries.")
```
### j.boolean()
```typescript theme={null}
j.boolean(): SchemaBuilder
```
Creates a boolean schema.
```typescript theme={null}
j.boolean().describe("Whether archived records should be included.")
```
### j.enum()
```typescript theme={null}
j.enum(...values: readonly T[]): SchemaBuilder
j.enum(...values: readonly T[]): SchemaBuilder
```
Creates an enum schema from string or number literal values.
```typescript theme={null}
j.enum("low", "medium", "high").describe("Priority level.")
```
### j.datetime()
```typescript theme={null}
j.datetime(): SchemaBuilder
```
Creates a string schema with `format: "date-time"`.
```typescript theme={null}
j.datetime().describe("ISO 8601 timestamp for the event.")
```
### j.date()
```typescript theme={null}
j.date(): SchemaBuilder
```
Creates a string schema with `format: "date"`.
```typescript theme={null}
j.date().describe("Due date in YYYY-MM-DD format.")
```
### j.time()
```typescript theme={null}
j.time(): SchemaBuilder
```
Creates a string schema with `format: "time"`.
```typescript theme={null}
j.time().describe("Start time.")
```
### j.duration()
```typescript theme={null}
j.duration(): SchemaBuilder
```
Creates a string schema with `format: "duration"`.
```typescript theme={null}
j.duration().describe("Elapsed time as an ISO 8601 duration.")
```
### j.email()
```typescript theme={null}
j.email(): SchemaBuilder
```
Creates a string schema with `format: "email"`.
```typescript theme={null}
j.email().describe("Email address for the assignee.")
```
### j.hostname()
```typescript theme={null}
j.hostname(): SchemaBuilder
```
Creates a string schema with `format: "hostname"`.
```typescript theme={null}
j.hostname().describe("Host name to query.")
```
### j.ipv4()
```typescript theme={null}
j.ipv4(): SchemaBuilder
```
Creates a string schema with `format: "ipv4"`.
```typescript theme={null}
j.ipv4().describe("IPv4 address to allow.")
```
### j.ipv6()
```typescript theme={null}
j.ipv6(): SchemaBuilder
```
Creates a string schema with `format: "ipv6"`.
```typescript theme={null}
j.ipv6().describe("IPv6 address to allow.")
```
### j.uuid()
```typescript theme={null}
j.uuid(): SchemaBuilder
```
Creates a string schema with `format: "uuid"`.
```typescript theme={null}
j.uuid().describe("External record ID.")
```
### j.anyOf()
```typescript theme={null}
j.anyOf[]>(
...schemas: S
): SchemaBuilder>
```
Creates an `anyOf` schema from the provided schema builders.
```typescript theme={null}
j.anyOf(j.string(), j.number()).describe("String or numeric identifier.")
```
### j.ref()
```typescript theme={null}
j.ref(path: string): SchemaBuilder
```
Creates a reference schema with `$ref` set to `path`.
```typescript theme={null}
j.ref("#/$defs/user")
```
Database schema helpers
Use `Schema` helpers to define the properties of managed databases declared with `worker.database()`.
```typescript theme={null}
import * as Schema from "@notionhq/workers/schema";
const tasks = worker.database("tasks", {
type: "managed",
initialTitle: "Tasks",
primaryKeyProperty: "Task ID",
schema: {
properties: {
Name: Schema.title(),
"Task ID": Schema.richText(),
Status: Schema.select([
{ name: "Open" },
{ name: "Done", color: "green" },
]),
},
},
});
```
### Schema.title()
```typescript theme={null}
Schema.title(): PropertyConfiguration
```
Creates a title property definition. This field becomes the title for all pages in the database. A database must have exactly one title property.
```typescript theme={null}
Name: Schema.title()
```
Returns:
```typescript theme={null}
{ type: "title" }
```
### Schema.richText()
```typescript theme={null}
Schema.richText(): PropertyConfiguration
```
Creates a rich text property definition.
```typescript theme={null}
"Task ID": Schema.richText()
```
Returns:
```typescript theme={null}
{ type: "text" }
```
### Schema.url()
```typescript theme={null}
Schema.url(): PropertyConfiguration
```
Creates a URL property definition.
```typescript theme={null}
Website: Schema.url()
```
Returns:
```typescript theme={null}
{ type: "url" }
```
### Schema.email()
```typescript theme={null}
Schema.email(): PropertyConfiguration
```
Creates an email property definition.
```typescript theme={null}
Email: Schema.email()
```
Returns:
```typescript theme={null}
{ type: "email" }
```
### Schema.phoneNumber()
```typescript theme={null}
Schema.phoneNumber(): PropertyConfiguration
```
Creates a phone number property definition.
```typescript theme={null}
Phone: Schema.phoneNumber()
```
Returns:
```typescript theme={null}
{ type: "phone_number" }
```
### Schema.checkbox()
```typescript theme={null}
Schema.checkbox(): PropertyConfiguration
```
Creates a checkbox property definition.
```typescript theme={null}
Done: Schema.checkbox()
```
Returns:
```typescript theme={null}
{ type: "checkbox" }
```
### Schema.file()
```typescript theme={null}
Schema.file(): PropertyConfiguration
```
Creates a file property definition.
```typescript theme={null}
Attachment: Schema.file()
```
Returns:
```typescript theme={null}
{ type: "file" }
```
### Schema.number()
```typescript theme={null}
Schema.number(format?: NumberFormat): PropertyConfiguration
```
Creates a number property definition. If `format` is provided, it is included in the returned configuration.
```typescript theme={null}
Amount: Schema.number("dollar")
```
Returns:
```typescript theme={null}
{ type: "number", format: "dollar" }
```
### Schema.date()
```typescript theme={null}
Schema.date(dateFormat?: DateFormat): PropertyConfiguration
```
Creates a date property definition. If `dateFormat` is provided, it is emitted as `date_format`.
```typescript theme={null}
Due: Schema.date("YYYY/MM/DD")
```
Returns:
```typescript theme={null}
{ type: "date", date_format: "YYYY/MM/DD" }
```
### Schema.select()
```typescript theme={null}
Schema.select(options: SelectOption[]): PropertyConfiguration
```
Creates a select property definition with predefined options.
```typescript theme={null}
Status: Schema.select([
{ name: "Open" },
{ name: "Done", color: "green" },
])
```
Returns:
```typescript theme={null}
{
type: "select",
options: [
{ name: "Open" },
{ name: "Done", color: "green" },
],
}
```
### Schema.multiSelect()
```typescript theme={null}
Schema.multiSelect(options: SelectOption[]): PropertyConfiguration
```
Creates a multi-select property definition with predefined options.
```typescript theme={null}
Tags: Schema.multiSelect([
{ name: "Bug", color: "red" },
{ name: "Feature", color: "blue" },
])
```
Returns:
```typescript theme={null}
{
type: "multi_select",
options: [
{ name: "Bug", color: "red" },
{ name: "Feature", color: "blue" },
],
}
```
### Schema.status()
```typescript theme={null}
Schema.status(config: { groups: StatusGroup[] }): PropertyConfiguration
```
Creates a status property definition with status groups.
```typescript theme={null}
State: Schema.status({
groups: [
{
name: "To-do",
options: [{ name: "Not started" }],
},
{
name: "In progress",
options: [{ name: "In progress", color: "blue" }],
},
{
name: "Complete",
options: [{ name: "Done", color: "green" }],
},
],
})
```
Returns:
```typescript theme={null}
{
type: "status",
groups: [
{ name: "To-do", options: [{ name: "Not started" }] },
{ name: "In progress", options: [{ name: "In progress", color: "blue" }] },
{ name: "Complete", options: [{ name: "Done", color: "green" }] },
],
}
```
### Schema.people()
```typescript theme={null}
Schema.people(): PropertyConfiguration
```
Creates a people property definition.
```typescript theme={null}
Assignees: Schema.people()
```
Returns:
```typescript theme={null}
{ type: "people" }
```
### Schema.place()
```typescript theme={null}
Schema.place(): PropertyConfiguration
```
Creates a place property definition for geographic locations.
```typescript theme={null}
Location: Schema.place()
```
Returns:
```typescript theme={null}
{ type: "place" }
```
### Schema.relation()
```typescript theme={null}
Schema.relation(
relatedDatabaseKey: string,
config?: { twoWay: false } | { twoWay: true; relatedPropertyName: string },
): PropertyConfiguration
```
Creates a relation property definition that references another database declared in the same worker. `relatedDatabaseKey` must match the key passed to `worker.database()` for the related database. If `config` is omitted, the relation is one-way.
```typescript theme={null}
const projects = worker.database("projects", {
type: "managed",
initialTitle: "Projects",
primaryKeyProperty: "Project ID",
schema: {
properties: {
Name: Schema.title(),
"Project ID": Schema.richText(),
},
},
});
const tasks = worker.database("tasks", {
type: "managed",
initialTitle: "Tasks",
primaryKeyProperty: "Task ID",
schema: {
properties: {
Name: Schema.title(),
"Task ID": Schema.richText(),
Project: Schema.relation("projects", {
twoWay: true,
relatedPropertyName: "Tasks",
}),
},
},
});
```
Returns:
```typescript theme={null}
{
type: "relation",
relatedDatabaseKey: "projects",
config: {
twoWay: true,
relatedPropertyName: "Tasks",
},
}
```
## Property value builders
Use `Builder` helpers to construct property values returned by sync changes. These properties must match the types defined in the [database schema](#database-schema-helpers).
```typescript theme={null}
return {
changes: [
{
type: "upsert",
key: task.id,
properties: {
Name: Builder.title(task.name),
"Task ID": Builder.richText(task.id),
Status: Builder.select(task.status),
},
},
],
hasMore: false,
};
```
### Builder.richText()
```typescript theme={null}
Builder.richText(content: string): TextValue
```
Creates a plain rich text value. `Builder.richText()` does not accept formatting
options; it returns a single text token with no annotations.
```typescript theme={null}
Builder.richText("task-123")
```
Returns:
```typescript theme={null}
[["task-123"]]
```
### Builder.url()
```typescript theme={null}
Builder.url(url: string): TextValue
```
Creates a URL value.
```typescript theme={null}
Builder.url("https://example.com")
```
Returns:
```typescript theme={null}
[["https://example.com"]]
```
### Builder.title()
```typescript theme={null}
Builder.title(content: string): TextValue
```
Creates a title value.
```typescript theme={null}
Builder.title("Write docs")
```
Returns:
```typescript theme={null}
[["Write docs"]]
```
### Builder.text()
```typescript theme={null}
Builder.text(content: string): TextValue
```
Creates a text value.
```typescript theme={null}
Builder.text("Imported from upstream.")
```
Returns:
```typescript theme={null}
[["Imported from upstream."]]
```
### Builder.email()
```typescript theme={null}
Builder.email(email: string): TextValue
```
Creates an email value.
```typescript theme={null}
Builder.email("person@example.com")
```
Returns:
```typescript theme={null}
[["person@example.com"]]
```
### Builder.phoneNumber()
```typescript theme={null}
Builder.phoneNumber(phone: string): TextValue
```
Creates a phone number value.
```typescript theme={null}
Builder.phoneNumber("+14155550123")
```
Returns:
```typescript theme={null}
[["+14155550123"]]
```
### Builder.checkbox()
```typescript theme={null}
Builder.checkbox(checked: boolean): TextValue
```
Creates a checkbox value. `true` returns `Yes`; `false` returns `No`.
```typescript theme={null}
Builder.checkbox(true)
```
Returns:
```typescript theme={null}
[["Yes"]]
```
### Builder.file()
```typescript theme={null}
Builder.file(fileUrl: string, fileName?: string): TextValue
```
Creates a file URL value. If `fileName` is omitted, the URL is also used as the display text.
```typescript theme={null}
Builder.file("https://example.com/invoice.pdf", "Invoice")
```
Returns:
```typescript theme={null}
[["Invoice", [["a", "https://example.com/invoice.pdf"]]]]
```
### Builder.number()
```typescript theme={null}
Builder.number(value: number): TextValue
```
Creates a number value by converting `value` to a string. If `value` is `NaN`, returns an empty value.
```typescript theme={null}
Builder.number(42)
```
Returns:
```typescript theme={null}
[["42"]]
```
### Builder.date()
```typescript theme={null}
Builder.date(dateString: string): TextValue
```
Creates a date value from a `YYYY-MM-DD` date string. Throws if the input does
not match that format or cannot be parsed by JavaScript `Date`.
```typescript theme={null}
Builder.date("2026-05-11")
```
Returns a date mention token:
```typescript theme={null}
[["\u2023", [["d", { type: "date", start_date: "2026-05-11" }]]]]
```
### Builder.dateTime()
```typescript theme={null}
Builder.dateTime(isoString: string, timeZone?: string): TextValue
```
Creates a datetime value from an ISO 8601 datetime string that starts with
`YYYY-MM-DDTHH:mm`. The builder stores the first 10 characters as `start_date`
and characters 11 through 16 as `start_time`. If `timeZone` is provided, it is
included as `time_zone`.
```typescript theme={null}
Builder.dateTime("2026-05-11T09:30:00Z", "America/Los_Angeles")
```
Returns a date mention token:
```typescript theme={null}
[
[
"\u2023",
[
[
"d",
{
type: "datetime",
start_date: "2026-05-11",
start_time: "09:30",
time_zone: "America/Los_Angeles",
},
],
],
],
]
```
### Builder.dateRange()
```typescript theme={null}
Builder.dateRange(startDate: string, endDate: string): TextValue
```
Creates a date range value from two `YYYY-MM-DD` date strings. Throws if either
input does not match that format or cannot be parsed by JavaScript `Date`. The
builder does not validate that `startDate` is before `endDate`.
```typescript theme={null}
Builder.dateRange("2026-05-11", "2026-05-15")
```
Returns a date mention token:
```typescript theme={null}
[
[
"\u2023",
[
[
"d",
{
type: "daterange",
start_date: "2026-05-11",
end_date: "2026-05-15",
},
],
],
],
]
```
### Builder.dateTimeRange()
```typescript theme={null}
Builder.dateTimeRange(
startDateTime: string,
endDateTime: string,
timeZone?: string,
): TextValue
```
Creates a datetime range value from two ISO 8601 datetime strings that start
with `YYYY-MM-DDTHH:mm`. If `timeZone` is provided, it is included as
`time_zone`. The builder does not validate that `startDateTime` is before
`endDateTime`.
```typescript theme={null}
Builder.dateTimeRange(
"2026-05-11T09:30:00Z",
"2026-05-11T10:30:00Z",
"America/Los_Angeles",
)
```
Returns a date mention token:
```typescript theme={null}
[
[
"\u2023",
[
[
"d",
{
type: "datetimerange",
start_date: "2026-05-11",
start_time: "09:30",
end_date: "2026-05-11",
end_time: "10:30",
time_zone: "America/Los_Angeles",
},
],
],
],
]
```
### Builder.link()
```typescript theme={null}
Builder.link(displayText: string, url: string): TextValue
```
Creates a text value with a link annotation.
```typescript theme={null}
Builder.link("Issue", "https://example.com/issues/123")
```
Returns:
```typescript theme={null}
[["Issue", [["a", "https://example.com/issues/123"]]]]
```
### Builder.select()
```typescript theme={null}
Builder.select(value: string): TextValue
```
Creates a select value from a single option name.
```typescript theme={null}
Builder.select("Open")
```
Returns:
```typescript theme={null}
[["Open"]]
```
### Builder.multiSelect()
```typescript theme={null}
Builder.multiSelect(...values: string[]): TextValue
```
Creates a multi-select value from option names. Values are joined with commas. If no values are provided, returns an empty value.
```typescript theme={null}
Builder.multiSelect("Bug", "Customer")
```
Returns:
```typescript theme={null}
[["Bug,Customer"]]
```
### Builder.status()
```typescript theme={null}
Builder.status(value: string): TextValue
```
Creates a status value from a status option name.
```typescript theme={null}
Builder.status("Done")
```
Returns:
```typescript theme={null}
[["Done"]]
```
### Builder.people()
```typescript theme={null}
Builder.people(...emails: string[]): PeopleValue
```
Creates a people value from email addresses.
```typescript theme={null}
Builder.people("a@example.com", "b@example.com")
```
Returns:
```typescript theme={null}
[{ email: "a@example.com" }, { email: "b@example.com" }]
```
### Builder.place()
```typescript theme={null}
Builder.place(value: PlaceValue): PlaceValue
```
Creates a place value. The value must include numeric `lat` and `lon`; otherwise the function throws.
```typescript theme={null}
Builder.place({
lat: 37.776,
lon: -122.417,
name: "San Francisco",
address: "San Francisco, CA",
})
```
Returns the provided place value.
### Builder.relation()
```typescript theme={null}
Builder.relation(primaryKey: string): RelationReference
```
Creates a relation reference from the primary key of a related record. Relation property values use arrays of relation references.
Single relation:
```typescript theme={null}
Project: [Builder.relation("project-123")]
```
Multiple relations:
```typescript theme={null}
Projects: [
Builder.relation("project-123"),
Builder.relation("project-456"),
]
```
Returns:
```typescript theme={null}
[
{ type: "primaryKey", value: "project-123" },
{ type: "primaryKey", value: "project-456" },
]
```
### Builder.emojiIcon()
```typescript theme={null}
Builder.emojiIcon(emoji: string): Icon
```
Creates an emoji icon.
```typescript theme={null}
Builder.emojiIcon("✅")
```
Returns:
```typescript theme={null}
{ type: "emoji", value: "✅" }
```
### Builder.notionIcon()
```typescript theme={null}
Builder.notionIcon(
icon: NoticonName,
color: NoticonColor = "gray",
): Icon
```
Creates an icon using Notion's native icon set. If `color` is omitted, it defaults to `"gray"`.
```typescript theme={null}
Builder.notionIcon("checkmark", "green")
```
Returns:
```typescript theme={null}
{ type: "notion", icon: "checkmark", color: "green" }
```
### Builder.imageIcon()
```typescript theme={null}
Builder.imageIcon(url: string): Icon
```
Creates an image icon from an external URL.
```typescript theme={null}
Builder.imageIcon("https://example.com/icon.png")
```
Returns:
```typescript theme={null}
{ type: "image", url: "https://example.com/icon.png" }
```
### Builder.imageCover()
```typescript theme={null}
Builder.imageCover(url: string, position?: number): Cover
```
Creates an image cover from an external URL. `position` controls the vertical image position from `0` (top) to `1` (bottom). If omitted, `position` defaults to `0.5`.
```typescript theme={null}
Builder.imageCover("https://example.com/cover.jpg", 0.25)
```
Returns:
```typescript theme={null}
{ type: "image", url: "https://example.com/cover.jpg", position: 0.25 }
```
# SDK reference
Source: https://developers.notion.com/workers/reference/sdk
Complete reference for the @notionhq/workers SDK.
The `@notionhq/workers` SDK defines a worker manifest in TypeScript. A worker exports one `Worker` instance, then registers resource declarations and capabilities on that instance.
```typescript src/index.ts theme={null}
import { Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
```
For database schemas, property value builders, and tool input schemas, see [Schema and builders](/workers/reference/schema).
## Worker
```typescript theme={null}
import { Worker } from "@notionhq/workers";
```
`Worker` is the entry point for every worker project. The class exposes methods
that add databases, pacers, and capabilities to the worker manifest.
| Method | Adds | Description |
| --------------------------------------- | ---------- | ---------------------------------------------------------------- |
| [`worker.database()`](#worker-database) | Database | Declares a managed Notion database for sync output. |
| [`worker.pacer()`](#worker-pacer) | Pacer | Declares a rate limit budget for calls to an external API. |
| [`worker.sync()`](#worker-sync) | Capability | Syncs upstream records into a managed Notion database. |
| [`worker.tool()`](#worker-tool) | Capability | Defines a callable tool with JSON Schema input and output. |
| [`worker.webhook()`](#worker-webhook) | Capability | Defines an HTTP webhook handler. |
| [`worker.oauth()`](#worker-oauth) | Capability | Defines OAuth configuration for external service authentication. |
worker.database()
Declares a managed Notion database for sync output.
```typescript theme={null}
const tasks = worker.database("tasks", {
type: "managed",
initialTitle: "Tasks",
primaryKeyProperty: "Task ID",
schema: {
properties: {
Name: Schema.title(),
"Task ID": Schema.richText(),
Status: Schema.select([{ name: "Open" }, { name: "Done" }]),
},
},
});
```
| Property | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
| `type` | Database declaration type. Currently only `"managed"` is supported. |
| `initialTitle` | Title used when Notion first creates the database. Changing this later does not rename an existing database. |
| `primaryKeyProperty` | Property used to match sync changes to Notion pages. Must be present in `schema.properties`. |
| `schema` | Database property schema. See [Schema and builders](/workers/reference/schema). |
### Managed database lifecycle
Each `worker.database()` declaration corresponds to a Notion database that the
platform creates for the worker. Managed databases are migrated on every deploy.
Changing the schema in code and redeploying migrates the database schema in
Notion.
Schema migrations can drop data. Review schema changes before deploying,
especially when removing or changing existing properties.
Managed databases are currently used only by sync capabilities. They are not
generic worker storage, and they are not used by webhooks.
The managed database schema must match the property values returned by sync
changes. In Notion, properties defined in code are not editable. Users can add
additional properties to the database, and those additional properties remain
editable in Notion.
`worker.database()` returns an opaque database handle. Pass that handle to `worker.sync()`.
worker.pacer()
Declares a rate limit budget for calls to an external API.
```typescript theme={null}
const issueTrackerApi = worker.pacer("issueTrackerApi", {
allowedRequests: 10,
intervalMs: 1000,
});
```
| Property | Description |
| ----------------- | -------------------------------------------------------- |
| `allowedRequests` | Maximum requests allowed per interval. |
| `intervalMs` | Interval length in milliseconds. |
| `wait()` | Promise that resolves when the next request can proceed. |
Call `await issueTrackerApi.wait()` before each external API request in a sync.
A pacer spreads requests across the configured window. The server injects a
wait-between period, and `wait()` sleeps the sandbox for that period before the
next request proceeds. For example, a pacer configured for 10,000 requests per
day spaces those requests across the day instead of allowing the worker to use
the full budget immediately.
This pacing can underutilise the upstream limit, but it keeps syncs making
progress throughout the pacer window. When multiple capabilities use the same
pacer, the server calculates the number of concurrently executing capabilities
and divides the pace across them.
worker.sync()
Registers a sync capability that writes changes into a [database](#worker-database). See the [Syncs guide](/workers/guides/syncs) for usage patterns, pagination, and scheduling.
```typescript theme={null}
worker.sync("tasksSync", {
database: tasks,
schedule: "30m",
execute: async (state) => {
const page = state?.page ?? 1;
await issueTrackerApi.wait();
const { items, hasMore } = await fetchTasks(page);
return {
changes: items.map((item) => ({
type: "upsert",
key: item.id,
properties: {
Name: Builder.title(item.name),
"Task ID": Builder.richText(item.id),
Status: Builder.select(item.status),
},
})),
hasMore,
nextState: hasMore ? { page: page + 1 } : undefined,
};
},
});
```
| Property | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------------- |
| `database` | Database handle returned from `worker.database()`. |
| `mode` | Sync lifecycle mode. Defaults to `"replace"`. |
| `schedule` | Run cadence. Defaults to `"30m"`. |
| `execute` | Function that fetches upstream data and returns sync changes. Receives the previous state as its first argument. |
### Execution cycle
A sync cycle is one or more `execute` calls. The runtime calls `execute` once,
applies the returned changes, and calls it again when `hasMore` is `true`. The
cycle completes when `hasMore` is `false`.
`state` is `undefined` on the first execution. To continue within a cycle, return
`hasMore: true` with a serialisable `nextState`; the runtime passes that value
as `state` to the next `execute` call.
### State
```typescript theme={null}
execute: async (state) => {
const cursor = state?.cursor;
const { items, nextCursor } = await fetchChanges(cursor);
return {
changes: items.map(toUpsert),
hasMore: Boolean(nextCursor),
nextState: nextCursor ? { cursor: nextCursor } : undefined,
};
}
```
`nextState` can be a cursor string, page number, timestamp, or object. Return it
whenever the next execution needs a cursor. If `hasMore` is `true`, `nextState`
must contain enough information for the next `execute` call to make progress.
Sync state persists across scheduled executions and deploys.
For incremental syncs against eventually consistent APIs, keep timestamp cursors
slightly behind the current time so recently written upstream records are not
skipped permanently:
```typescript theme={null}
const bufferedNow = new Date(Date.now() - 15_000).toISOString();
const latestReturnedCursor = records.at(-1)?.updatedAt;
const cursor =
latestReturnedCursor && latestReturnedCursor < bufferedNow
? latestReturnedCursor
: bufferedNow;
return {
changes: records.map(toUpsert),
hasMore: false,
nextState: { cursor },
};
```
### Modes
| Mode | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"replace"` | Each completed sync cycle represents the full upstream dataset. After `hasMore: false`, records not seen in the cycle are deleted. |
| `"incremental"` | Each completed sync cycle represents only changed records. Existing records not mentioned are left unchanged; deletes must be returned explicitly. |
Use `mode: "replace"` for small sources or full backfills. Use
`mode: "incremental"` for delta syncs that fetch only records changed since the
last cursor.
### Schedule
```typescript theme={null}
worker.sync("hourlySync", {
database: tasks,
schedule: "1h",
execute: async () => ({
changes: [],
hasMore: false,
}),
});
```
Use `"continuous"`, `"manual"`, or an interval string ending in `m`, `h`, or
`d`. Interval schedules must be at least `1m` and at most `7d`.
| Schedule | Description |
| ----------------------- | ---------------------------------------- |
| `"continuous"` | Runs as frequently as the system allows. |
| `"manual"` | Runs only when explicitly triggered. |
| `"15m"`, `"1h"`, `"1d"` | Runs at the specified interval. |
If `schedule` is omitted, the sync runs every 30 minutes.
### Sync result
```typescript theme={null}
const result = {
changes: [],
hasMore: false,
};
```
For paginated syncs, return `hasMore: true` with a serialisable `nextState`:
```typescript theme={null}
const result = {
changes,
hasMore: true,
nextState: { cursor: "next-page-cursor" },
};
```
| Property | Description |
| ----------- | ---------------------------------------------------------------------------------------------------------------------- |
| `changes` | Batch of upsert and delete changes to apply. |
| `hasMore` | `true` when the runtime should call `execute` again with `nextState`; `false` when the current sync cycle is complete. |
| `nextState` | Optional serialisable cursor or pagination state for the next execution. Required when `hasMore` is `true`. |
Return batches sized for the upstream API and sync runtime. A batch of about 100
changes is a typical starting point.
### Sync changes
```typescript theme={null}
const upsert = {
type: "upsert",
key: "task-123",
properties: {
Name: Builder.title("Write SDK docs"),
"Task ID": Builder.richText("task-123"),
Status: Builder.select("Open"),
},
icon: Builder.notionIcon("document", "blue"),
cover: Builder.imageCover("https://example.com/cover.jpg", 0.5),
upstreamUpdatedAt: "2026-05-11T09:30:00Z",
pageContentMarkdown: "Imported from the upstream task tracker.",
};
```
Use `"delete"` to remove a record by upstream key:
```typescript theme={null}
const deleteChange = {
type: "delete",
key: "task-123",
};
```
| Property | Description |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `type` | `"upsert"` creates or updates a record. `"delete"` removes a record. |
| `key` | Upstream record identifier. This should match the value stored in the database primary key property. |
| `targetDatabaseKey` | Optional database key override. Defaults to the database associated with the sync. |
| `properties` | Upsert-only property values. Keys must match the database schema. See [Schema and builders](/workers/reference/schema). |
| `upstreamUpdatedAt` | Optional ISO 8601 timestamp used for conflict resolution when multiple syncs write to the same database. |
| `icon` | Optional page icon. Use `Builder.emojiIcon()`, `Builder.notionIcon()`, or `Builder.imageIcon()`. |
| `cover` | Optional page cover. Use `Builder.imageCover()` with an external image URL and optional vertical position. |
| `pageContentMarkdown` | Optional markdown page body content. |
Delete changes are only applicable in `mode: "incremental"`. In
`mode: "replace"`, the runtime deletes records that were not seen by the end of
the completed sync cycle.
### Multiple syncs for one database
Multiple syncs can write to the same database by passing the same database
handle. A common pattern is a manual replace-mode backfill sync plus a scheduled
incremental delta sync:
```typescript theme={null}
worker.sync("tasksBackfill", {
database: tasks,
mode: "replace",
schedule: "manual",
execute: async (state) => {
const page = state?.page ?? 1;
await issueTrackerApi.wait();
const { items, hasMore } = await fetchAllTasks(page);
return {
changes: items.map(toTaskUpsert),
hasMore,
nextState: hasMore ? { page: page + 1 } : undefined,
};
},
});
worker.sync("tasksDelta", {
database: tasks,
mode: "incremental",
schedule: "5m",
execute: async (state) => {
const cursor = state?.cursor;
await issueTrackerApi.wait();
const { items, nextCursor } = await fetchChangedTasks(cursor);
return {
changes: items.map(toTaskUpsert),
hasMore: Boolean(nextCursor),
nextState: nextCursor ? { cursor: nextCursor } : undefined,
};
},
});
```
Both syncs must use unique sync keys. When multiple syncs share a pacer, the
server apportions the request budget across them. Use `upstreamUpdatedAt` on
upsert changes when multiple syncs can update the same record.
worker.tool()
Registers a tool that can be called by Notion Custom Agents. See the [Agent tools guide](/workers/guides/tools) for a walkthrough of defining inputs, testing locally, and deploying.
Tools extend custom agent functionality. A worker is attached to a custom agent,
and each tool declared by that worker can be enabled or disabled on that
connection.
```typescript theme={null}
worker.tool("searchTasks", {
title: "Search tasks",
description: "Searches tasks by query.",
schema: j.object({
query: j.string().describe("Search query."),
}),
outputSchema: j.object({
results: j.array(j.string()),
}),
hints: {
readOnlyHint: true,
},
execute: async ({ query }) => {
const results = await searchTasks(query);
return { results };
},
});
```
| Property | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `title` | Human-readable tool name shown in Notion. |
| `description` | Description of what the tool does and when it should be used. |
| `schema` | Input schema built with `j` from `@notionhq/workers/schema-builder`. See [Schema and builders](/workers/reference/schema). |
| `outputSchema` | Optional output schema built with `j`. When present, tool output is validated before it is returned. |
| `hints` | Optional advisory metadata for Notion Custom Agents. |
| `execute` | Function called with validated input and a capability context. The return value must be JSON-serialisable. |
### Tool hints
```typescript theme={null}
hints: {
readOnlyHint: true,
}
```
Tool hints describe how the tool behaves. `readOnlyHint: true` marks a tool as
read-only and safe to auto-execute. Tools without `readOnlyHint: true` are
treated as write tools and prompt for confirmation.
worker.webhook()
Registers an HTTP webhook endpoint for external services. See the [Webhooks guide](/workers/guides/webhooks) for request verification, retries, and using the Notion API from a webhook handler.
```typescript theme={null}
worker.webhook("onGithubPush", {
title: "GitHub push",
description: "Handles GitHub push events.",
execute: async (events) => {
for (const event of events) {
if (!verifySignature(event.rawBody, event.headers)) {
throw new WebhookVerificationError("Invalid signature");
}
}
},
});
```
| Property | Description |
| ------------- | --------------------------------------------------------------------- |
| `title` | Human-readable webhook name shown in Notion. |
| `description` | Description of what the webhook handles. |
| `execute` | Function called when the webhook receives events. |
| `deliveryId` | Unique ID for this webhook delivery, stable across retries. |
| `body` | Parsed JSON body, or an empty object when the body is not valid JSON. |
| `rawBody` | Raw request body. Use this for signature verification. |
| `headers` | HTTP headers from the incoming request. |
| `method` | HTTP method, such as `"POST"`. |
Throw `WebhookVerificationError` from `execute` to signal signature verification failure. After five consecutive verification failures, the platform rejects incoming requests for the webhook without executing the handler.
worker.oauth()
Registers a user-managed OAuth provider and returns an OAuth capability handle. See the [OAuth guide](/workers/guides/oauth) for the full setup flow, including provider configuration and testing locally.
OAuth capabilities require an OAuth app configured with the external provider.
Use the provider's client ID, client secret, authorisation endpoint, token
endpoint, and scopes in the capability configuration. Store credentials as [secrets](/workers/guides/secrets), not in code.
```typescript theme={null}
const githubAuth = worker.oauth("githubAuth", {
name: "GitHub",
clientId: process.env.GITHUB_CLIENT_ID ?? "",
clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "",
authorizationEndpoint: "https://github.com/login/oauth/authorize",
tokenEndpoint: "https://github.com/login/oauth/access_token",
scope: "repo user",
});
```
| Property | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| `name` | OAuth provider instance name. Used to identify the connected token. |
| `clientId` | OAuth app client ID. |
| `clientSecret` | OAuth app client secret. |
| `authorizationEndpoint` | OAuth 2.0 authorisation endpoint. |
| `tokenEndpoint` | OAuth 2.0 token endpoint. |
| `scope` | OAuth scopes requested during authorisation. |
| `authorizationParams` | Optional extra query parameters for the authorisation request. |
| `callbackUrl` | Optional OAuth redirect URL override. |
| `accessTokenExpireMs` | Optional default access token expiry in milliseconds when the provider does not return expiry information. |
### OAuth setup
Use the redirect URL from the CLI when configuring the OAuth app with the
provider:
```shell theme={null}
ntn workers oauth show-redirect-url
```
After the worker is deployed, start the three-legged OAuth flow for the OAuth
capability:
```shell theme={null}
ntn workers oauth start githubAuth
```
`githubAuth` is the capability key passed as the first argument to
`worker.oauth()`.
Use `accessToken()` from tool, sync, or webhook handlers to read the connected token.
```typescript theme={null}
const token = await githubAuth.accessToken();
```
After the OAuth flow completes, the server stores the refresh token and refreshes
access tokens automatically according to the access token expiry. If the token
response does not include expiry information, `accessTokenExpireMs` supplies the
default expiry interval.