# 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, `ntn login` automatically falls back to a two-step flow: 1. Run `ntn login` on the headless machine. 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. 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`: ```bash theme={null} curl -fsSL https://ntn.dev | bash ``` `ntn` is available for macOS and Linux (x64 and arm64). Windows support coming soon! ## Install via npm ```bash theme={null} npm install --global ntn ``` Requires Node.js 22+ and npm 10+. ## 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 update ` | 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 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. * **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. * **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. * **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. * **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. * **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. * **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. * **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. * **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 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\_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.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.server\_connected**: An MCP server was connected to the workspace. * **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.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. # Connect Cursor to a custom agent Source: https://developers.notion.com/guides/agents/connect-cursor-to-custom-agent Learn how to connect Cursor to a Notion custom agent so it can build features, fix bugs, and open pull requests from Notion tasks. This feature is currently in **beta**. The setup flow and capabilities may change. This guide walks you through connecting [Cursor](https://cursor.com) to a Notion custom agent. Once connected, your agent can turn a page, task, or comment into working code and a pull request — and it can keep running in the background while you move on to other work. With this connection, you can: * **Create PRs from Notion** — Start a pull request from a page, task, or comment * **Fix bugs from tasks** — Point the agent at a bug report and let it produce a fix * **Run in the background** — Close the page and come back later to find a PR link waiting ## Prerequisites Before you start, make sure you have: * A [custom agent](https://www.notion.com/help/custom-agent) in your Notion workspace * A [Cursor](https://cursor.com) account (logging in with GitHub gives the easiest setup) During setup, you'll create a **Cursor User API Key** and paste it into Notion. ## Set up the connection ### 1. Add the Cursor connection in Notion Open the custom agent you want to connect. Open the agent's **Settings**. In **Tools & access**, click **Add connection**. Add connection button in the agent's Tools & access settings Select **Cursor** from the dropdown and click **Connect**. Selecting Cursor from the connection dropdown ### 2. Create a User API Key in Cursor Log in to [cursor.com](https://cursor.com). For the easiest setup, log in with GitHub. If you log in another way, connect GitHub to your Cursor account first. Open the [Integrations tab](https://cursor.com/dashboard?tab=integrations) in your Cursor dashboard. Create a new **User API Key** and copy it. Creating a User API Key in the Cursor dashboard ### 3. Finish the connection in Notion Return to Notion and paste your Cursor User API Key into the connection modal, then click **Connect**. Pasting the Cursor API token into the Notion connection modal Share the pages and databases your agent needs to read. This typically includes your engineering task database and any spec pages the agent should reference. Sharing pages and databases with the agent Save your changes to the agent. Cursor is now connected and your agent can write code and open pull requests directly from your tasks or agent chat. ## Using the agent There are two ways to hand work to your agent: * **Assign a database page** (like a task) to the agent * **@mention the agent** in a comment on a page or task When you do, be direct about what you want it to build and where the work should land. For example: * *"Create a PR from this spec in \[repo URL]"* * *"Start this in the background, then open a PR when it's ready."* * *"Fix the bug described in this task and update the task."* Assigning a task to the agent with a clear instruction Your agent can keep working after you close the page or agent chat. To check on progress, open the agent and look at its chat activity for status updates and links. ## Troubleshooting Progress lives in the agent's **chat activity**, even if you started the work from a task comment. Open the agent to see status updates and PR links. Use the agent's **chat** when you want a single thread of status updates. Use **task comments** when you want the work to stay attached to a specific page. Share the relevant pages and databases with the agent so it can read the spec, task, and any linked context. You can update shared pages in the agent's **Settings** → **Tools & access**. Give it a minute, then confirm: * Cursor is connected (check **Settings** → **Tools & access**) * Your API key is still valid * The agent has access to the task and any referenced pages Reconnect Cursor in **Tools & access** using a fresh API key. The old key is no longer valid after rotation. # 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.so/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 ``` ![Caption](URL) {color="Color"} ``` ```html theme={null} Caption Caption ``` ### Page and database references ```html theme={null} Title Title ``` ### 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 name Page title Database name Data source name Agent 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. # 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.so/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.so/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.so/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 [Add comment to page](/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. ## 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 [Add comment to page](/reference/create-a-comment) endpoint can also be used to respond to a discussion thread on a 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.page_id`. Inline comments cannot be directly added to blocks to start a new discussion using the public API. Currently, the API can only be used to respond to inline comments (discussions). #### Retrieving a discussion ID The 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.so/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.so/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.so/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.so/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.so/help/wikis-and-verified-pages#create-a-wiki) and [verifying pages](https://www.notion.so/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.so/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.so/{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** In order to find all the pages in a data source, send a request to the [query a data source](/reference/query-a-data-source) without a `filter` parameter. 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/pagination) explains how to use the `start_cursor` and `page_size` parameters to get more than 100 results. ### 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 or retrieve files and media from Notion pages. 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` * 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) | `![caption](url)` | | [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\n\nParagraph"`. 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\n\nDiscussed roadmap priorities.\n\n## Action items\n\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\n\nDiscussed roadmap priorities.\n\n## Action items\n\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\n\nDiscussed roadmap priorities.\n\n## Action items\n\n- [ ] Draft proposal\n- [ ] Schedule follow-up", "truncated": false, "unknown_block_ids": [] } ``` ### 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\n\nFirst section content...\n\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 standard markdown with actual newline characters. In your JSON request body, use `\n` to encode newlines — for example, `"## Heading\n\nParagraph text"` creates a heading followed by a paragraph. 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\n\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\n\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\n\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\n\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\n\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\n\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\n\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\n\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\n\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\n\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. ## 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.so/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/page#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/errors#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/errors#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 are read-only. To access the generated content, fetch the page's children to find the meeting notes block, then follow the child block IDs to retrieve each section's content. **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.so/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.so/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.so/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 auth flow set-up 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.so/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 should act as your primary key when storing information. ### 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", }), }); ... ``` # Best practices for handling API keys Source: https://developers.notion.com/guides/get-started/handling-api-keys Learn how to manage and secure your Notion API tokens. API keys are powerful credentials that provide access to Notion through our Public API. This guidance applies to internal connection tokens, OAuth access tokens, and [personal access tokens](/guides/get-started/personal-access-tokens). If these keys fall into the wrong hands, they can pose serious security risks to your connections, data, and workspace. ## Why leaked API keys are dangerous When your Notion API key is exposed, malicious actors could potentially: * **Access sensitive data**: Read all pages, databases, and content in your workspace * **Modify or delete content**: Make unauthorized changes to your workspace data * **Export your data**: Download and steal your intellectual property * **Perform actions on your behalf**: Create, update, or delete pages and databases * **Access user information**: View workspace members and their permissions The scope of access depends on the permissions granted to the connection or token. For PATs, the token acts as the user who created it, so leaked PATs can expose anything that user can access with the token's capabilities. ## Best practices ### *NEVER* share your API keys * Keep your API key private: Treat your API key like your personal password—don’t share it with anyone. If others need access, they should request their own key or create their own PAT. * Never post your key publicly: Avoid sharing your API key in public spaces such as forums, emails, or support tickets, with the Notion support team. * Be careful with third-party tools: Uploading your API key to external services will provide your key to those services. Only share your key with trusted services. Always store your API key as an encrypted secret when working with third-party platforms. Never put your key directly into code or configuration files - use environment variables! ### Use environment variables Never hardcode API keys directly in your source code. Instead, use environment variables: ```bash theme={null} # .env file (never commit this file) NOTION_API_KEY=ntn_abc123def456ghi789jkl012mno345pqr ``` ```typescript TypeScript theme={null} // In your code const notion = new Client({ auth: process.env.NOTION_API_KEY, }); ``` ### Secure your environment files * Add `.env` files to your `.gitignore` to prevent accidental commits * Use different API keys for development, staging, and production environments * Store production keys in secure secret management systems like AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault ### Implement secret scanning Use tools like [GitLeaks](https://github.com/gitleaks/gitleaks), [Detect Secrets](https://github.com/Yelp/detect-secrets), [Trufflehog](https://github.com/trufflesecurity/trufflehog), or [BitPatrol](https://bitpatrol.io/) to automatically detect and prevent the commitment of sensitive information like API keys to your repositories. These tools can: * Scan your codebase for potential secrets before commits * Integrate with CI/CD pipelines for continuous scanning * Alert developers when secrets are accidentally committed ### Regular key rotation * Rotate API keys on a schedule and set calendar reminders to do so. PATs expire one year after creation, so plan replacements before they expire. * Immediately rotate keys when team members with access leave * Keep an inventory of all API keys and their purposes ## What should I do if I suspect my API key has been compromised? If you suspect that your API key may be compromised, we recommend taking action immediately: ### Step 1 - Revoke the compromised key Log into your Notion account Go to **Settings & members** → **Connections** → **Develop or manage connections** Find the connection or personal access token with the compromised key Click **Refresh** on your connection, or revoke the personal access token ### Step 2 - Generate a new API key Rotate the compromised key by clicking **Refresh** in your connections page and update your applications with your new key. For a compromised PAT, revoke the old token and create a new PAT. ### Step 3 - review recent activity Check your workspace for any suspicious activity Review recent changes to pages and databases Look for any unauthorized connections in **Settings & members** → **Connections** ### Step 4 - update your applications * Replace the old API key in all your applications and environments * Test that your connections are working with the new key * Remove the old key from any configuration files or documentation ## Getting help If you need assistance with API key security or suspect unauthorized access, contact [Notion support](https://www.notion.com/help) at [team@makenotion.com](mailto:team@makenotion.com) # 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.so/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.so/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.so/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.so/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 the 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.so/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.so/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. ## Starting your connection journey We recommend starting with an internal connection — it's the fastest way to begin building. You get an API token immediately and can focus entirely on using the API within your workspace, without worrying about OAuth or Marketplace listing. You can always create a public connection later if you need multi-workspace support. Here's a guided path through the documentation: [**Quickstart**](/guides/get-started/quick-start) — Build your first connection with a hands-on tutorial. [**Personal access tokens**](/guides/get-started/personal-access-tokens) — Create a user-scoped token for scripts, CLI workflows, Workers, or trusted tools. [**Internal connections**](/guides/get-started/internal-connections) — Understand how internal connections work, including the permissions model. [**Public connections**](/guides/get-started/public-connections) — Learn how public connections work, including installation scope and the OAuth flow. [**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) are user-scoped bearer tokens. A PAT belongs to one Notion user in one workspace, and it uses that user's workspace membership and page permissions when it calls the Notion API. PATs are useful when you want to authenticate as yourself without creating an [internal connection](/guides/get-started/internal-connections) or implementing 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 where one Notion user is the right security boundary: * 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. Do not use a PAT as the auth model for a product used by many Notion users. For that, create a [public connection](/guides/get-started/public-connections) so each user authorizes access with OAuth. If you need a stable workspace-level bot for a team-owned automation, use an [internal connection](/guides/get-started/internal-connections). ## How PATs work A PAT is created in the Developer portal. When you create one, you choose: * A token name. * The workspace the token belongs to. * Capabilities for the token. PATs can have two capability bundles: | 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 Notion API capability is controlled by the workspace's PAT creation policy. The Workers capability can be granted to workspace members, but Workers feature availability is still checked when you use Workers. 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} const { Client } = require("@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. PATs are intentionally different from internal connections. An internal connection operates as a separate bot user and only accesses pages explicitly shared with that connection. A PAT uses a real user's permissions, so it is best for user-owned workflows 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 & members → Connections**: * View all PATs created in the workspace, including active 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 PATs. * Configure who can create PATs with Notion API access. Admins cannot reveal or copy another member's token secret. Only the token creator can reveal or copy their own PAT. 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. ### 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, selected groups are managed from the PAT creators settings page. If no groups are selected, workspace owners are the only users who can create PATs with Notion API access. ## Create a PAT Open the Developer portal. Go to Personal access tokens. Click **New personal access token**. Enter a name, choose a workspace, and select the capabilities the token should have. Click **Create token**, then copy the token value and store it securely. PATs expire one year after creation. Create a new PAT and update your scripts or tools before the old token expires. ## 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 & members → 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 Treat PATs like 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. # Developer quickstart Source: https://developers.notion.com/guides/get-started/quick-start Build your first Notion connection with a hands-on tutorial. ## Why start with an internal connection? We recommend starting with an [internal connection](/guides/get-started/internal-connections). Internal connections let you focus on building with the API right away — you just need an API token to get started. There's no OAuth flow to implement and no listing process to worry about. Everything stays within your workspace. If you later need your connection to work across multiple workspaces, you can always create a [public connection](/guides/get-started/public-connections) and re-wire your code to use OAuth. If you only need a token for a user-owned script or CLI workflow, a [personal access token](/guides/get-started/personal-access-tokens) may be faster. This quickstart uses an internal connection because it demonstrates a team-owned automation with explicit page sharing. In this guide, we're going to build an internal Notion connection that creates a new database in your workspace via a web form. ## What to know before you start Before diving in, here are a few essential things to keep in mind when working with the Notion API: * **Versioning:** Every request must include a `Notion-Version` header. See [Versioning](/reference/versioning) for the latest version. * **Rate limits:** The API allows an average of 3 requests per second. If you exceed this, you'll receive a `429` response with a `Retry-After` header. See [Request limits](/reference/request-limits). * **Error handling:** API errors return structured JSON with a `code` and `message`. See [Status codes](/reference/status-codes) for the full list. * **Pagination:** List endpoints return paginated results. Use `start_cursor` and `has_more` to iterate through pages. See the [API introduction](/reference/intro) for details. * **Token security:** Never store your API secret in source code or version control. Use environment variables or a secret manager. See [Best practices for handling API keys](/guides/get-started/handling-api-keys). ## What you will build This guide will demonstrate how to build an HTML form that will [create a new Notion database](/reference/create-a-database) when submitted. By the end of this guide, we'll have a functional app that looks like this: The completed [sample code](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/web-form-with-express) includes additional examples beyond what's covered in this guide, including forms to: * [Add a new page](/reference/post-page) to the database * [Add content](/reference/patch-block-children) to the new page * [Add a comment](/reference/create-a-comment) to the page content ### Requirements To follow along with this guide, you will need: * A [Notion account](https://www.notion.so/signup). * To be a [Workspace Owner](https://www.notion.so/help/add-members-admins-guests-and-groups) in the workspace you're using. You can create a new workspace for testing purposes otherwise. * Knowledge of HTML and JavaScript. We'll use [Express.js](https://expressjs.com/) for a server, as well. * [npm and Node.js](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed locally to use the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js) and [Express.js](https://expressjs.com/) **SDK usage is recommended, but not required** The [sample code](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/web-form-with-express) shown below uses the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js) to make public API requests. Using the Notion SDK for JavaScript is not required to build a Notion connection, but many JavaScript developers prefer it due to its ease of use. ## Create your connection in Notion The first step is to create a new internal connection in Notion's Developer portal. In the **Build** section of the sidebar, select **Internal connections**, then click **Create a new connection**. Enter the connection name and select the workspace where the connection can be installed. ### Get your access token API requests require an installation access token to be successfully authenticated. Visit the `Configuration` tab to get your connection's installation access token. **Never share your installation access token!** Any value used to authenticate API requests should always be kept private. Use environment variables and avoid committing sensitive data to your version control history. If you do accidentally expose it, remember to refresh your installation access token. [Learn more: Best practices for handling API keys](/guides/get-started/handling-api-keys) ### Give your connection page permissions The database that we're about to create will be added to a parent Notion page in your workspace. For your connection to interact with the page, it needs explicit permission to read/write to that specific Notion page. To give the connection permission, you will need to: Pick (or create) a Notion page. Click on the `...` More menu in the top-right corner of the page. Scroll down to `+ Add Connections`. Search for your connection and select it. Confirm the connection can access the page and all of its child pages. Your connection can now make API requests related to this Notion page and any of its children. To learn more about how internal connection permissions work — including the bot identity model — see the [Internal connections](/guides/get-started/internal-connections) guide. **Double-check your page access** If your API requests are failing, confirm you have given the connection permission to the page you are trying to update. This is a common cause of API request errors. ## Setting up the demo locally In this example, we'll have three key files: * `index.html`, which will contain our client-side HTML. * `client.js`, which will contain our client-side JavaScript code. * `server.js`, which will contain our server-side JavaScript code. This file contains all the endpoints to make requests to Notion's public API, as well as to serve the `index.html` file. ([More on that below.](#step-3-importing-the-notion-sdk-serverjs)) All of the sample code is available in [GitHub](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript/web-form-with-express). **Various examples are available** This connection includes frontend code, but connections can be server-side only, as well. See more examples of different connection use cases in [GitHub](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript). ### Clone demo repo To run this project locally, clone the repo and install its dependencies ([Express.js](https://expressjs.com/en/starter/installing.html), [dotenv](https://www.npmjs.com/package/dotenv), and [Notion's SDK for JavaScript](https://github.com/makenotion/notion-sdk-js)): ```bash Shell theme={null} # Clone this repository locally git clone https://github.com/makenotion/notion-cookbook.git # Switch into this project cd notion-cookbook/examples/javascript/web-form-with-express/ # Install the dependencies npm install ``` ### Environment variables In your `.env` file, add the following variables: ```bash .env theme={null} NOTION_KEY= NOTION_PAGE_ID= ``` Add the access token you retrieved in the [Get your access token](#get-your-access-token) step to `NOTION_KEY`, as well as a page ID (`NOTION_PAGE_ID`) for the page that you gave the connection permission to update. **How database IDs work** When using the API to [create a database](/reference/create-a-database), the parent of a database must be a Notion page or a [wiki](https://www.notion.so/help/wikis-and-verified-pages) database. To get the ID of the page, locate the 32-character string at the end of the page's URL. The page ID is highlighted. As a best practice, add `.env` to your `.gitignore` file to ensure you don't accidentally share these values. ### Running the project locally To run this project locally, you will need to enter the following command in your terminal: ```bash Shell theme={null} npm start ``` Once the server is running, open [http://localhost:8000](http://localhost:8000) in your browser. Next, let's look at how our database form works. ## Creating a new database ### Step 1 - Build the form In our `index.html` file, we need a form for the user to create a new database and an area for the API response to be displayed. This is how the user will initiate a Notion API request. The corresponding [HTML elements](https://github.com/makenotion/notion-cookbook/blob/main/examples/javascript/web-form-with-express/views/index.html#L40) related to creating a database are shown below: ```html HTML expandable theme={null} ... ... ... ...

1. Create a new database

... ```
In terms of what's rendered in the ``, notice the `
` element and an empty table cell with the ID `dbResponse`. The latter is where we'll append the Notion API response information. The database form includes two inputs: * A text input for the database name * A submit input to submit the form Also of note: the `client.js` file is included in the document's `` tag, which allows us to apply client-side JavaScript to interact with these HTML elements. ### Step 2 - Handle the submission In `client.js`, we can write a function to describe what should happen when the database form is submitted. In short, we want to make a request to `server.js` to then make an API request to Notion. The actual Notion API request will happen server-side to avoid exposing our API secret in the client. (In other words, it's more secure!) ```jsx JSX expandable theme={null} // Assign the database form to a variable for later use const dbForm = document.getElementById("databaseForm"); // Assign the empty table cell to a variable for later use const dbResponseEl = document.getElementById("dbResponse"); // Add a submit handler to the form dbForm.onsubmit = async function (event) { event.preventDefault() // Get the database name from the form const dbName = event.target.dbName.value const body = JSON.stringify({ dbName }) // Make a request to /databases endpoint in server.js const newDBResponse = await fetch("/databases", { method: "POST", headers: { "Content-Type": "application/json", }, body, }) const newDBData = await newDBResponse.json() // Pass the new database info and the empty table cell // to a function that will append it. appendApiResponse(newDBData, dbResponseEl) } ``` In this code block, we select the form element using its ID attribute with `getElementbyId()`. Next, we attach an async function to the `onsubmit` event that will make a request to our local server's `/databases` endpoint. (This endpoint will be described below in our `server.js` code.) The function is asynchronous because we need to wait for a response from our server before proceeding. The response is then appended to our `index.html` document. ([More on this below.](#step-5-displaying-the-response-indexhtml)) ### Step 3 - Set up the Notion SDK Let's start by looking at our `server.js` file without the Notion-related endpoints: ```javascript JavaScript expandable theme={null} require("dotenv").config(); const express = require("express"); const app = express(); // Notion SDK for JavaScript const { Client } = require("@notionhq/client"); const notion = new Client({ auth: process.env.NOTION_KEY }); // app.use(express.static("public")); // app.get("/", function(request, response) { response.sendFile(__dirname + "/views/index.html"); }); // listen for requests const listener = app.listen(process.env.PORT, function() { console.log("Your app is listening on port " + listener.address().port); }); ``` This Express.js code will listen for requests to `/` (e.g., `localhost:/`) and respond with the `index.html` file. That's how the app knows to render our `index.html` code when the server is started. To use the SDK, we import it at the top of `server.js`. We also initialize a new Notion Client instance and set the `auth` option to the Notion API secret already set in the environment variables: ```jsx JSX theme={null} const { Client } = require("@notionhq/client"); const notion = new Client({ auth: process.env.NOTION_KEY }); ``` We can now make requests to Notion's API in this file without having to worry about authentication again. The SDK automatically sets the `Notion-Version` header on every request, so you don't need to include it manually. If you're making requests without the SDK, you'll need to set this header yourself. See [Versioning](/reference/versioning) for details. ### Step 4 - Send the API request Staying in `server.js`, we can add the following code that will be invoked when the database form makes a POST request to `/databases`: ```jsx TypeScript expandable theme={null} app.post("/databases", async function (request, response) { const pageId = process.env.NOTION_PAGE_ID; const title = request.body.dbName; try { // Notion API request! const newDb = await notion.databases.create({ parent: { type: "page_id", page_id: pageId, }, title: [ { type: "text", text: { content: title, }, }, ], properties: { Name: { title: {}, }, }, }); response.json({ message: "success!", data: newDb }); } catch (error) { response.json({ message: "error", error }); } }); ``` `app.post()` indicates this endpoint is for POST requests, and the first argument (`"/databases"`) indicates this function corresponds to requests made to the `/databases` path, as we did in our client-side code above. Next, we can actually interact with the Notion API. To create a new database, we'll use the [Create a database](/reference/create-a-database) endpoint: ```jsx TypeScript theme={null} await notion.databases.create({...options}) ``` To use this endpoint, we need to pass the parent page ID in the body parameters. This page ID is the one already set in the environment variables. The page ID **must** be included in this request. ```jsx TypeScript theme={null} const pageId = process.env.NOTION_PAGE_ID; ... try { const newDb = await notion.databases.create({ parent: { type: "page_id", page_id: pageId, }, ... ``` (Note: Environment variables can only be accessed in `server.js` , not `client.js`.) In this example, the title of the database should also be set. The title was provided in the form the user submitted, which we can access from the request's body (`request.body.dbName`). ```jsx TypeScript theme={null} const pageId = process.env.NOTION_PAGE_ID; const title = request.body.dbName; // Get the user's title try { const newDb = await notion.databases.create({ parent: {...}, title: [ { type: "text", text: { content: title, // Include the user's title in the request }, }, ], // ... ``` Finally, we need to describe the [database's properties](/reference/property-object). The properties represent the columns in a database (or the "schema", depending on which terminology you prefer.) In this case, our database will have just one column called "Name", which will represent the page names of its child pages: ```jsx TypeScript theme={null} try { const newDb = await notion.databases.create({ parent: {...}, title: [...], properties: { Name: { title: {}, }, }, }) ... ``` Finally, assuming the request works, we can return the response from Notion's API back to our original fetch request in `client.js`: ```jsx JavaScript theme={null} ... response.json({ message: "success!", data: newDb }); ``` If it doesn't work, we'll return whatever error message we get from Notion's API: ```jsx JavaScript theme={null} try { ... } catch (error) { response.json({ message: "error", error }); } ``` Now that we have our new database, the response can be added to the HTML document via the client-side JavaScript (`client.js`). ### Step 5 - Display the response Let's first look at an example of the object the `/databases` endpoint responds with, which includes the database object that gets returned from the Notion API when we create a new database: ```json Response expandable theme={null} { message: "success!", data: { // from Notion object: "database", id: "e604f78c-4145-4444-b7d5-1adea4fa5d08", cover: null, icon: null, created_time: "2023-07-18T20:56:00.000Z", created_by: { object: "user", id: "44b170f0-16ac-47cf-aaaa-8f2eab66hhhh" }, last_edited_by: { object: "user", id: "44b170f0-16ac-47cf-gggg-8f2eab6rrrra", }, last_edited_time: "2023-07-18T20:56:00.000Z", title: [ { type: "text", text: [Object], annotations: [Object], plain_text: "test db", href: null, }, ], description: [], is_inline: false, properties: { Name: { id: "title", name: "Name", type: "title", title: {} }, }, parent: { type: "page_id", page_id: "e7261079-9d30-4313-9999-14b29880gggg", }, url: "", public_url: null, archived: false, in_trash: false }, } ``` The most important information here (for our purposes) is the database ID (`data.id`). The ID will be required to make API requests to the [Create a page](/reference/post-page) endpoint, which is the next form in our completed demo's UI. Knowing this JSON structure, let's now look at how `appendApiResponse()` works: ```jsx JSX expandable theme={null} const dbForm = document.getElementById("databaseForm"); // Empty table cell where we'll display the API response const dbResponse = document.getElementById("dbResponse"); ... // Appends the API response to the UI const appendApiResponse = function (apiResponse, el) { // Add success message to UI const newParagraphSuccessMsg = document.createElement("p") newParagraphSuccessMsg.innerHTML = "Result: " + apiResponse.message el.appendChild(newParagraphSuccessMsg) // See browser console for more information if there's an error if (apiResponse.message === "error") return // Add ID of Notion item (db, page, comment) to UI const newParagraphId = document.createElement("p") newParagraphId.innerHTML = "ID: " + apiResponse.data.id el.appendChild(newParagraphId) // Add URL of Notion item (db, page) to UI if (apiResponse.data.url) { const newAnchorTag = document.createElement("a") newAnchorTag.setAttribute("href", apiResponse.data.url) newAnchorTag.innerText = apiResponse.data.url el.appendChild(newAnchorTag) } } ``` `appendApiResponse(res, form)` accepts two parameters: the response (shown above) and the HTML element where we will append the response — in this case, an empty table cell next to the database form. In this function, we first add a paragraph element to show the response message (i.e., whether it was a success or the error). ```jsx JSX theme={null} const newParagraphSuccessMsg = document.createElement("p") newParagraphSuccessMsg.innerHTML = "Result: " + apiResponse.message el.appendChild(newParagraphSuccessMsg) ``` Then, we do the same with the database ID after confirming the response was not an error: ```jsx JSX theme={null} if (apiResponse.message === "error") return // Add ID of database and data source to UI const newParagraphId = document.createElement("p") newParagraphId.innerHTML = "Database ID: " + \ apiResponse.data.id + "; Data Source ID" + apiResponse.data.data_sources[0].id el.appendChild(newParagraphId) ``` Finally, if the response has a URL, we display that too with an anchor (``) tag. This allows the user to visit the database directly in Notion. ```jsx JSX theme={null} if (apiResponse.data.url) { const newAnchorTag = document.createElement("a") newAnchorTag.setAttribute("href", apiResponse.data.url) newAnchorTag.innerText = apiResponse.data.url el.appendChild(newAnchorTag) } ``` (Note: This function will be reused by other forms. Not all responses have a `url` property, which is why we check for it.) Once this is done, our HTML document is updated and the form submission is officially complete. ## Testing the feature Let's see the final results of testing this new feature: The database form is submitted and the response from Notion's API is appended to our UI. We can click the link to visit the new database in Notion and confirm it worked as expected. As a next step, the new database ID can be copy and pasted into the page form below it to create a new page in the database. We can also use the page ID that the page form returns to add content to the page or comment on it using the block and comment forms. We won't cover the code for page, blocks, or comment forms here, but the code is all included in the [source code](https://github.com/makenotion/notion-cookbook/blob/main/examples/javascript/web-form-with-express/views/index.html) for reference. It works the same as the database example. As a next step, you could also try adding a feature to [retrieve all existing pages](/reference/query-a-data-source) in the database, or [retrieve block children](/reference/get-block-children) (i.e., page content) for an existing page. ## Wrapping up This guide demonstrated how to use Notion's public API (via the [Notion SDK for JavaScript](https://github.com/makenotion/notion-sdk-js)) to build an internal connection. With this demo app, users can programmatically create a new database in their Notion workspace by filling out a form in the app UI and making a request to Notion's public API — the [Create a database](/reference/create-a-database) endpoint. As a reminder, this example includes client-side code to allow for user interactions via a GUI (graphical user interface). Notion connections do not require a UI, however. What you build is completely up to you! To see examples of server-side-only connections, test out the sample apps in the SDK's [GitHub repo](https://github.com/makenotion/notion-cookbook/tree/main/examples/javascript). ## Next steps Learn how internal connection permissions and authentication work. Expand to multiple workspaces with OAuth 2.0. Implement the full OAuth flow for public connections. # 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](https://www.notion.so/developers/connections) 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. # Building a link preview Source: https://developers.notion.com/guides/link-previews/building-a-link-preview Follow this step-by-step guide to create your first link preview. A Link Preview is a real-time excerpt of authenticated content that unfurls in Notion when an authenticated user pastes a supported link in their workspace. Developers can build Link Preview connections to customize how links unfurl in Notion workspaces for domains they own.
connection's settings. To learn how to create a public connection, follow the [Authorization guide](/guides/get-started/authorization). ## Configure Link Preview settings in the Developer portal This step will guide you through enabling link previews for your connection, as well as filling out the link preview connection forms found in your connection settings. To start, navigate to the public connection you will be using for your link preview connection. It can be found in the Developer portal. If you have access to the Link Preview API, you will see a Link Preview section in the Configuration tab. This page contains a toggle input to enable link previews for your connection. Switching it on will display the External Authorization Setup form that will you need to fill out and save. Once the link preview toggle is turned on for the connection, you will need to fill out two forms in the following order: 1. The External Authorization Setup form. 2. The Unfurling Domain & Patterns form. In the next section, we'll review how to fill out these forms. **If you don't see `Enable link preview` as an option, then:** * Make sure that you’ve [applied](https://notionup.typeform.com/to/BXheLK4Z?typeform-source=developers.notion.com) and received access to the Link Previews API. * Confirm that you’re logged in to the Notion account that you used to request access. * Reach out to **[developers@makenotion.com](mailto:developers@makenotion.com)** if you continue to have issues. ### 1. Fill out the External Authorization Setup form The External Authorization Setup settings give Notion the information that it needs to let a user authenticate with your service when they paste a Link Preview enabled URL in Notion. | Field | Description | Example value | | :------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------- | | OAuth Authorize URL | The URL that Notion redirects the user to when they connect your connection to their account.

When Notion redirects to this URL, it passes a `code` as a query param in the request. Your connection trades the `code` for an `access_token` to make authenticated requests to the Link Previews APIs. | `https://.com/notion/authorize` | | OAuth Token URL | The URL that responds to a Notion POST request with an `access_token` from your service.

Notion uses the `access_token` to make authenticated requests to your systems. | `https://.com/notion/token` | | OAuth Client ID | The client ID that Notion uses in its requests to your Authorize and OAuth Token URLs. | `mRkZGFjM` | | OAuth Client Secret | The client secret that Notion uses in its requests to the Authorize and Token URLs. | `ZGVmMjMz` | | OAuth Scopes (optional) | An optional scopes string for Notion to send as a parameter in the request to your OAuth Authorize URL. | `unfurl`, `user_name` | | Deleted Token Callback URL (optional) | A URL that Notion sends a DELETE request to when a user removes a Link Preview from a Notion page or disconnects your connection from their workspace, so that you can delete their tokens.

You can use the request body that Notion sends to look up the user and deactivate their associated `access_token`s from your service.

Whether or not you use a deleted token callback, Notion invalidates any Notion-side tokens corresponding to the user and the Link Preview that they delete. | `https://.com/notion/deletion` | After you’ve filled out this information, click `"Submit ->"` to continue to Unfurling Domain & Patterns. ### 2. Fill out the Unfurling Domain & Patterns form The Unfurling Domain & Patterns settings give Notion the information that it needs to recognize the URLs that you want to unfurl Link Previews. | Field | Description | Example value | | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------- | | Unfurl Callback URL | The URL that shares data to be displayed in the Link Preview with Notion. Notion sends POST and DELETE requests to this URL when a user adds or deletes a Link Preview.

You can leave this blank as you set up OAuth and return to it once you’ve created your unfurl attributes. Refer to [Use the Unfurl Callback URL](#use-the-unfurl-callback-url) for details.

Must be an internet-accessible URL that can and should be protected by authentication. | `https://.com/unfurl` | | Unfurl URL Domain | The root domain that maps to this connection.

After you add a domain, follow the prompts to verify your domain with Notion. | `.com` | | URL matching and placeholder | The pattern that a URL must match in order to unfurl as a Link Preview.

You can provide multiple patterns for one connection. | Refer to the table below. | The URL matching and placeholder field includes its own fields: | Field | Description | Example value | | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Rule name | A name for the pattern. | `"item"` | | Sample URLs | An example URL that matches the pattern and triggers a Link Preview. | `https://acme.com/items/23487` | | Pattern | A Regex pattern that Notion can use to identify URLs that trigger Link Previews.

If the Regex pattern fails to match any sample URL, then an error prompt appears when you save settings. | `^(?https\:\/\/acme\.com)\/items\/(?\d+)$` | | Unfurl Regex Attributes | An array of JSON objects. Each JSON object contains placeholders, populated from Regex capture groups, for a Link Preview’s unfurl attributes. Placeholders are displayed when a Link Preview is waiting for data to populate from your service.

You can leave this blank as you set up OAuth and return to it once you’ve created your unfurl attributes. | `[ { "id": "title", "name": "Title", "type": "inline", "inline": { "title": { "value": "Acme Item #$", "section": "title" } } }, { "id": "itemId", "name": "Item Id", "type": "inline", "inline": { "plain_text": { "value": "#$", "section": "identifier" } } }, { "id": "dev", "name": "Developer Name", "type": "inline", "inline": { "plain_text": { "value": "Acme Inc", "section": "secondary" } } } ]` | After you've filled out the External Authorization Setup and Unfurling Domain & Patterns settings, click `"Submit ->"` to create the connection. ## Set up the authorization flow There are two high-level parts to the auth flow for a Link Preview: * **Your service authenticates with Notion.** Notion sends a `code` to your OAuth Authorize URL. Your connection exchanges this `code` for a Notion `access_token` that enables your service to make authenticated requests to the Link Previews APIs. * **Notion authenticates with your service.** Your service responds to Notion’s request with a `code`. Notion exchanges this token for your service’s `access_token` via your OAuth Token URL. This allows Notion to embed the data from your service in Link Previews. The tokens **need to be exchanged the first time** a user attempts to add your Link Preview enabled URL to a page. After the initial exchange, the Notion `access_token` is long-living and doesn’t need to be updated. If you prefer, Notion can also [support refresh tokens](#how-to-use-refresh-tokens-optional) and fetch new tokens from your service. The auth flow begins when a user shares your Link Preview enabled URL in Notion. Notion recognizes the link and redirects to the OAuth Authorize URL that you provided in the connection settings. Notion includes the following query params to kick off the OAuth flow with your service: | Parameter | Description | Value | | :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------- | | `code` | A UUID that Notion generates to authenticate with your service. | `614846ff-b061-4eac-a511-fc20c3f0838a` (example value) | | `redirect_uri` | A constant string. Your service redirects a user to this URL after they grant permission for it to access Notion.

To prevent attackers from providing arbitrary URIs, your service should validate that the redirect URI matches this value. | `https://notion.so/externalIntegrationAuthCallback` | | `client_id` | The OAuth Client ID that you provided when you created the connection. | `mRkZGFjM` (example value) | | `scope` (optional) | The OAuth Scopes value that you provided when you created the connection. | `unfurl`, `user_name` (example values) | | `response_type` | A constant string. | `code` | | `state` | A randomized string for security validation. | `tga@YNV9cfw4yrv0thw` (example value) | Your implementation begins after Notion sends the request. ### 1. Provide OAuth form Listen for requests to your OAuth Authorize URL. When you detect a request from Notion, present a UI that asks the user to allow the authentication process to continue. For example, Slack shares the following interstitial when a user initiates a Link Preview from a Slack URL: ### 2. Authenticate with Notion’s `access_token` In your connection implementation, retrieve the `code` that Notion sent when it called your OAuth Authorize URL. Then, send the `code` as part of a POST request to Notion’s token URL: `https://api.notion.com/v1/oauth/token`. The Notion `code` is valid for 10 minutes. If the `code` expires, then an error is returned and you need to reinitiate the auth flow for Notion to authenticate with your service ([Step 1](#1-provide-oauth-form)). To explore other possible errors, refer to the [OAuth 2.0 documentation](https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/). Default to re-initiating the auth flow to handle errors. The request is authorized using HTTP Basic Authentication. The credential is a colon-delimited combination of the connection's `CLIENT_ID` and `CLIENT_SECRET`: `CLIENT_ID:CLIENT_SECRET`. You can find these values on the connection settings page. Find your connection, and click `View connection`. 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. Notion also requires the word `Basic` before the `base64` encoded string. A complete code param looks something like the following: `Basic NjQ5Mzc0OTIzNzQ5MjM4NDc5MjM4NDc5MjM0NzkyMzc0OjQ3Mzg5Mjc0OTIzODQ3Mjk0ODcyMzkzNDgyNzk0ODcyMzQ5` For more information, read about HTTP Basic Authentication in our [Authorization guide](/guides/get-started/authorization#step-3-send-the-code-in-a-post-request-to-the-notion-api). The body of the request contains the following JSON-encoded fields: | Parameter | Description | Value | | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------- | | `code` | A unique random code that Notion generates to authenticate with your service, generated when a user initiates the auth flow. | `"ABC"` (example value) | | `grant_type` | A constant string. | `authorization_code` | | `external_account` | Object with `key` and `name` properties.

`key` should be a unique identifier for the account. Notion uses the `key` to determine whether or not the user is re-connecting the same account.

`name` should be some way for the user to know which account they used to authenticate with your service. | Refer to the example below. | The following example demonstrates an `external_account` object for `team@makenotion.com`, a Notion employee account, to authenticate with Slack to use a Slack Link Preview. ```json JSON theme={null} { "key": "A83823453409384", "name": "Notion - team@makenotion.com" } ``` The account `name` appears in the `"My connections"` settings page, where a user can review their authentications for your connection. A complete POST request looks like the below: ```curl cURL theme={null} curl --location --request POST 'https://api.notion.com/v1/oauth/token' \ --header 'Authorization: Basic '"$BASE64_ENCODED_ID_AND_SECRET"'' \ --header 'Content-Type: application/json' \ --header 'Notion-Version: 2026-03-11' \ --data '{ "grant_type": "authorization_code", "code": "e202e8c9-0990-40af-855f-ff8f872b1ec6", "external_account": { "key": "A83823453409384", "name": "Notion - team@makenotion.com" } }' ``` Notion responds to the request with a 200 OK and the following response body: | Parameter | Description | Value | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------- | | `access_token` | A unique random string that Notion generates, in exchange for the `code`. You can use the `access_token` to make authorized requests to the Notion API. | `"ABC"` (example value) | | `bot_id` | A UUID representing this authorization. | `"3d592781-2dcc-4d4b-bcf3-776a2a7ad7b8"` (example value) | | `duplicated_template_id` | Always `null` for Link Preview connections. Create a [standard public connection](/guides/get-started/overview#connection-types) to use template URLs with the API. | `null` | | `owner` | An object indicating who owns the authorized workspace. | Refer to the [bot object](/reference/user#bots) documentation. | | `token_type` | A constant string. | `"bearer"` | | `workspace_id` | A UUID representing the ID of the Notion workspace where the authorization flow took place. | `"3d592781-2dcc-4d4b-bcf3-776a2a7ad7b8"` (example value) | | `workspace_icon` | A URL to an image, or a string of characters, that identifies the workspace. This could be useful if you’d like to display this authorization in your service’s UI. | `"🍩"` | | `workspace_name` | A string representing a human-readable name that can be used to display this authorization in your service’s UI. | `"My Team Workspace"` (example value) | Store the Notion response, and associate it with the user who initiated the OAuth flow. Notion stores the token that you provided. For tips on storing `access_token`s, check out [the auth guide](/guides/get-started/authorization#step-5-the-connection-stores-the-access_token-and-refresh_token-for-future-requests). ### 3. Redirect to the `redirect_uri` with `code` When a user selects `"Allow"` to grant Notion the requested permissions, redirect to the `redirect_uri` , the constant string [`notion.so/externalIntegrationAuthCallback`](http://notion.so/externalIntegrationAuthCallback), with your service’s unique `code` and the `state` that Notion sent to your service when it initiated the auth flow. When Notion receives the redirect, it sends a POST request to the OAuth Token URL that you provided with the following body: | Parameter | Description | Value | | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------ | | `code` | A unique random string that your service generates, retrieved from your service’s request to the `redirect_uri`. Notion is sending back the code that you sent. | `WQtaEYNV9jfL4yr89KJA0thw` | | `client_id` | The OAuth Client ID that you provided when you created the connection. | `mRkZGFjM` (example value) | | `client_secret` | The OAuth Client Secret that you provided when you created the connection. | `ZGVmMjMz` (example value) | | `redirect_uri` | A constant string. | `notion.so/externalIntegrationAuthCallback` | | `grant_type` | A constant string. | `authorization_code` | The body is sent in the `application/x-www-form-urlencoded` format and expects a JSON response. ### 4. Share the `access_token` with Notion From your OAuth Token URL, respond to Notion’s POST request with an `access_token` body parameter in your 200 response. Notion saves the `access_token` to send in future requests. Notion sends a request to your Unfurl Callback URL every time that the user associated with the token pastes a new Link Preview enabled URL or revisits a page with an existing Link Preview to refresh data. **How Notion handles OAuth errors** Notion handles error responses as described by the [OAuth Error Spec](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1). The message that Notion displays to the user varies depending on the information that you provide. For example, you can respond with an `error` and a standard error code like `access_denied`: `https://notion.so/externalintegrationauthcallback?error=access_denied` In this instance, Notion shares the following message: You can also add an `error_description` to the response: `https://notion.so/externalintegrationauthcallback?error=access_denied&error_description=The+user+has+denied+your+application+access` If Notion detects a description, then it replaces the standard dialogue prompt with the specified description, as in the below: If Notion doesn’t recognize the error code, then it notes that the error is unknown: For more details on standard error codes, refer to the [OAuth spec](https://www.rfc-editor.org/rfc/rfc6749#section-4.2.2.1). #### How to use refresh tokens (optional) Skip to [Step 5](#5-test-the-auth-flow-in-notion) if you’re creating long-living access tokens. This section only applies to temporary access tokens. ##### Return a `refresh_token` Instead of creating a long-living `access_token`, you can send a `refresh_token` alongside a temporary access token. Notion can then use the `refresh_token` to fetch new tokens from your service. If you return a `refresh_token`, then you need to also return an `expires_in` integer, as in the following example: ```json Example 200 JSON response from your service with refresh_token and expires_in values theme={null} { "access_token": "ABC", "refresh_token": "XYZ", "expires_in": 60000 } ``` `expires_in` represents the number of seconds until the `access_token` expires. ##### Notion requests to refresh a token If Notion detects that the `access_token` is expired, meaning that the current time exceeds the time of the last refresh plus the `expires_in` value, then Notion refreshes the tokens when it calls your endpoints. To refresh the token, Notion sends a POST request to your OAuth Token URL with the following parameters: | Parameter | Description | Value | | :-------------- | :------------------------------------------------------------------------- | :-------------------------------------------------- | | `refresh_token` | The unique random string returned in the response to the previous request. | `"ABC"` (example value) | | `client_id` | The OAuth Client ID that you provided when you created the connection. | `mRkZGFjM` (example value) | | `client_secret` | The OAuth Client Secret that you provided when you created the connection. | `ZGVmMjMz` (example value) | | `redirect_uri` | A constant string. | `https://notion.so/externalIntegrationAuthCallback` | | `grant_type` | A constant string. | `refresh_token` | ##### Respond to Notion’s request to refresh a token From your OAuth Token URL, return an `access_token` in your 200 response. You can optionally return new `refresh_token` and `expires_in` values. ### 5. Test the auth flow in Notion To test the auth flow, make sure that you’ve added the connection to a workspace. Then, navigate to `"My connections"` in the workspace settings. Click `"Show all"`. Find the connection that you created in the list, and select `"Connect"` to kick off the auth flow. If you don’t see your connection in the list, then refresh the page. Notion only loads new connections on page load. If the auth flow is successful, then you’ll see a new entry under the `"My connections"` menu. To verify that the `key` for this connection is unique, repeat the auth flow multiple times using the same credentials to validate that you only get a single entry. ## Use the Unfurl Callback URL ### 1. Configure unfurl attributes After a user pastes a Link Preview enabled link and completes the auth flow, Notion sends a POST request to the Unfurl Callback URL that you provided in the connection settings. The request includes a Bearer authorization with the user’s `access_token` from your service, and the payload is a single field called `uri` that includes the link that the user shared: ```curl cURL theme={null} curl -d '{"uri":"http://example.com/file/123"}' \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " -X POST https://example.com/unfurl ``` Set up the Unfurl Callback URL to respond to Notion’s request with a 200 OK including the `uri`, and an array of all of the unfurl attributes, the values to display in the Link Preview. **The array must include a `title` attribute that gives the Link Preview a title and a *dev* attribute that indicates the developer or company that created the Link Preview**. The following is an example response: ```json JSON expandable theme={null} { "uri": "http://example.com/file/123", "operations": [ { "path": [ "attributes" ], "set": [ { "id": "title", "name": "Title", "type": "inline", "inline": { "title": { "value": "The Link Preview's Title", "section": "title" } } }, { "id": "dev", "name": "Developer Name", "type": "inline", "inline": { "plain_text": { "value": "Acme Inc", "section": "secondary" } } }, { "id": "color", "name": "Color", "type": "inline", "inline": { "color": { "value": { "r": 235, "g": 64, "b": 52 }, "section": "background" } } } ] } ] } ``` To preview how different response objects unfurl in a Link Preview, explore the connection's Link Preview Lab. To learn more about unfurl attributes, refer to the Link Preview [unfurl attributes reference](/reference/unfurl-attribute-object). ### 2. Handle unfurl request errors Set up the Unfurl Callback URL to handle errors, as in the following example. ```json JSON theme={null} { "uri": "http://example.com/file/123", "operations": [ { "path": ["error"], "set": { "status": 404, "message": "Content not found" } } } ``` ## Manage updates to Link Previews ### Update Link Previews to reflect data shared in unfurl attributes If the unfurl attributes from your service change over time, then you can alert Notion to update the Link Preview to mirror those changes. When your service detects changes to data that is referenced by a Link Preview, send a PATCH request to Notion’s `/v1/external/object` endpoint to update the unfurl attributes. Include all of the same objects from the Unfurl Callback URL response in the request, including the attributes that haven’t changed, as in the following example: ```bash cURL expandable theme={null} curl -d `{ "uri": "http://example.com/file/123", "operations": [ { "path": ["attributes"], "set": [ { "id": "title", "name": "Title", "type": "inline", "inline": { "title": { "value": "The Link Preview's NEW Title", "section": "title" } } }, { "id": "color", "name": "Color", "type": "inline", "inline": { "color": { "value": { "r": 235, "g": 64, "b": 52 }, "section": "background" } } } ] } ] }` \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "Notion-Version: 2021-08-16" \ -X PATCH https://api.notion.com/v1/external/object ``` It’s also possible to set a new error request. For example, if the data originally shared in a Link Preview can’t be found, then you could send an update request as follows: ```bash cURL expandable theme={null} curl -d `{ "uri": "http://example.com/file/123", "operations": [ { "path": ["error"], "set": { "status": 404, "message": "Content not found" } } ] }` \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "Notion-Version: 2021-08-16" \ -X PATCH https://api.notion.com/v1/external/object ``` You can also set both new attributes and an error request at the same time, as in the below example: ```bash cURL expandable theme={null} curl -d `{ "uri": "http://example.com/file/123", "operations": [ { "path": ["attributes"], "set": [ { "id": "title", "name": "Title", "type": "inline", "inline": { "title": { "value": "The Link Preview's Title", "section": "title" } } }, { "id": "color", "name": "Color", "type": "inline", "inline": { "color": { "value": { "r": 235, "g": 64, "b": 52 }, "section": "background" } } } ] }, { "path": ["error"], "set": { "status": 404, "message": "Content not found" } } ] }` \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "Notion-Version: 2021-08-16" \ -X PATCH https://api.notion.com/v1/external/object ``` When updating a Link Preview’s unfurl attributes, there’s no need to clear the `error`. If no `error` is sent, then the `error` is automatically cleared. #### Notion updates your service when a user deletes Link Preview enabled URLs When a user deletes all Link Previews associated with a URL from their workspace, Notion sends a DELETE request to your Unfurl Callback URL. Listen for the request to perform any associated actions, like deleting the record from your service. ## Submit your connection for security review Before a Link Preview connection can be publicly distributed, it needs to pass a security review. [Fill out this form](https://docs.google.com/forms/d/e/1FAIpQLSd94UcRziV-1yFv6udO0qZwohLyXxhYYadUqEJyyEd03RAj1w/viewform) to submit your connection for review. **Next steps** * To learn more about customizing a Link Preview’s unfurl attributes, refer to the [reference docs](/reference/unfurl-attribute-object) # Introduction Source: https://developers.notion.com/guides/link-previews/introduction Learn how link previews work and what you need to build them. 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.