Visitar URL original
Notion | Chat SDK

Notion

Join Notion page and block comment discussions through webhooks and the Comments API, with Post+Edit streaming.

Create a new bot

Scaffold a Next.js bot with the Notion adapter using create-chat-sdk:

pnpm create chat-sdk@latest my-bot --adapter notion memory -y

The generated app uses the Memory state adapter for local development. To add the adapter to an existing app, start at Install.

Install

pnpm add @chat-adapter/notion

Quick start

The adapter auto-detects credentials from NOTION_TOKEN, NOTION_VERIFICATION_TOKEN, and optional NOTION_BOT_USERNAME / NOTION_VERSION / NOTION_MENTION_MODE / NOTION_KEYWORDS.

For managed credentials, see Vercel Connect.

lib/bot.ts
import { Chat } from "chat";
import { createNotionAdapter } from "@chat-adapter/notion";
import { createRedisState } from "@chat-adapter/state-redis";

const bot = new Chat({
  userName: "my-bot",
  adapters: {
    notion: createNotionAdapter(),
  },
  state: createRedisState(),
});

bot.onNewMention(async (thread, message) => {
  await thread.post("Hello from Notion!");
});
app/api/webhooks/notion/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";

export async function POST(request: Request): Promise<Response> {
  return bot.webhooks.notion(request, {
    waitUntil: (task) => after(() => task),
  });
}

Platform setup

The adapter targets a single workspace through an access-token connection.

Create the connection

  1. Open the Developer Portal and click New connection.
  2. Enter a connection name, choose Access token as the authentication method, and select the workspace the connection is installable in. Only one workspace is supported, and the connection is installed automatically. Click Create connection.
  3. On the connection page, copy the Access token and set it as NOTION_TOKEN.

Set capabilities

  1. Under Capabilities then Comment capabilities, enable Read comments and Insert comments.
  2. Under Capabilities then Content capabilities, leave Read content, Update content, and Insert content as they are. Read content is required for message.subject page metadata and for verifying the containing page before editing or deleting block-parent comments.
  3. Under Capabilities then User capabilities, leave Read user information including email addresses as it is. The adapter uses it for author names and mention resolution.

Choose content access

On the Content access tab, choose which pages and databases the connection can access. The connection is available, and webhooks fire, only for those pages and databases.

Capability or access errors, typically HTTP 403, usually mean a comment capability is missing or the page or database was not enabled under Content access.

Authentication

Vercel Connect

Use Vercel Connect for the outbound Notion access token:

lib/bot.ts
import { createNotionAdapter } from "@chat-adapter/notion";
import { connectNotionAdapter } from "@vercel/connect/chat";

const notion = createNotionAdapter({
  ...connectNotionAdapter("notion/acme-notion"),
  verificationToken: process.env.NOTION_VERIFICATION_TOKEN,
});

Connect does not forward Notion webhooks. Configure the subscription directly in Notion and retain NOTION_VERIFICATION_TOKEN for native HMAC verification. NOTION_TOKEN is not needed when using connectNotionAdapter.

Create the connector and attach it to your project with the Vercel CLI. Notion webhooks go directly to your app, so omit --triggers. vercel env pull downloads a development OIDC token for local runs:

vercel link
vercel connect create notion --name acme-notion
vercel connect attach notion/acme-notion
vercel env pull

Access token

Set NOTION_TOKEN to the access token you copied in Platform setup, or pass it as token.

Configuration

PropTypeDefaultDescription
token?string | (() => string | Promise<string>)
Connection access token or resolver invoked per API request. Auto-detected from `NOTION_TOKEN`.
verificationToken?string
Webhook HMAC key from subscription verification. Auto-detected from `NOTION_VERIFICATION_TOKEN`.
notionVersion?string"2026-03-11"
Value for the `Notion-Version` header. Auto-detected from `NOTION_VERSION`. Change it at your own risk: the adapter is tested against the pinned version (see API version).
userName?string"notion-bot"
Bot display name. Auto-detected from `NOTION_BOT_USERNAME`.
mentionMode?"mention" | "all-comments" | "keyword""mention"
How inbound comments become Chat SDK mentions. Default `"mention"` matches plain-text `@userName` / `@botUserId`. Auto-detected from `NOTION_MENTION_MODE`. See Mention detection.
keywords?string[]
Case-insensitive word-boundary keywords when `mentionMode` is `"keyword"`. Auto-detected from `NOTION_KEYWORDS` (comma-separated).
streamingEditIntervalMs?number1500
Minimum interval between Post+Edit streaming edits (ms). Tuned for Notion's ~3 req/s average connection rate limit.
externalUrlPollDelaysMs?number[][0, 5000, 10000]
Delays (ms) between Retrieve File Upload polls after an `external_url` create. Default is an immediate recheck, then 5s / 10s (~15s max) so serverless webhook handlers stay within typical timeouts. Use Notion's longer `[5000, 15000, 30000, 45000]` when imports are slow and the caller can wait; `[]` skips waiting and links unless create already returned `uploaded`.
apiBaseUrl?string"https://api.notion.com/v1"
Override the Notion API base URL (constructor-only; not from env). Useful for tests and proxies. Backs `customApiEndpoint`.

token and verificationToken are required at runtime, from the environment or config.

Environment variables

VariableRequiredDescription
NOTION_TOKENYes, unless using Vercel ConnectConnection access token (Bearer token).
NOTION_VERIFICATION_TOKENYesHMAC key from the webhook verification handshake.
NOTION_BOT_USERNAMENoBot display name override (default notion-bot).
NOTION_MENTION_MODENomention | all-comments | keyword (default mention).
NOTION_KEYWORDSNoComma-separated keywords when NOTION_MENTION_MODE=keyword.
NOTION_VERSIONNoOverride the Notion-Version header (default pinned in API version).

Webhooks

You create subscriptions on the connection's Webhooks tab, not through the API.

  1. Deploy your app so https://your-domain.com/api/webhooks/notion is publicly reachable.
  2. On the connection page, open the Webhooks tab and click Create a subscription.
  3. Set the Webhook URL, leave the default API version as it is, and under Events deselect every category except Comment. This selects Comment created, Comment deleted, and Comment updated. The adapter requires Comment created; the base adapter acknowledges and ignores deleted and updated events.
  4. Notion sends a one-time POST containing verification_token. The adapter logs it at warn level and returns 200.
  5. Paste that token into Notion's UI and click Verify.
  6. Set the same value as NOTION_VERIFICATION_TOKEN (or pass verificationToken in config), then restart so signed deliveries can be verified.

After verification, the adapter checks every delivery with HMAC-SHA256 over the raw body against X-Notion-Signature (sha256=<hex>).

Event-ID deduplication is best-effort: in memory, plus state-backed when a Chat state adapter is configured. It does not substitute for Notion's delivery semantics across cold starts unless the state adapter is durable.

Notion locks the webhook URL after verification and does not let you change it in place. To point at a new endpoint, delete the subscription and create a new one, then repeat the verification-token flow and update NOTION_VERIFICATION_TOKEN. Choose a stable production URL before verifying.

Mention detection

ModeBehavior
"mention" (default)isMention when the comment plain text contains @userName or @botUserId. Notion connection bots are not @-mentionable in the composer, so users type the @ token manually.
"all-comments"Every non-bot comment on connected pages is treated as a mention, which suits dedicated Q&A pages.
"keyword"isMention when the comment matches any configured keywords (case-insensitive, word-boundary). Comments without a keyword are left undetermined, so Chat SDK still matches @userName in the text.
createNotionAdapter({
  userName: "docs-bot",
  // default mentionMode: "mention" → triggers on "@docs-bot" or "@<bot-user-id>"
});
createNotionAdapter({
  mentionMode: "keyword",
  keywords: ["@docs-bot", "hey bot"],
});

Streaming and rate limits

Notion allows a connection to update its own comments, so the adapter uses Post+Edit streaming: it posts the first chunk, then PATCHes the same comment as tokens arrive.

Notion's average rate limit is about 3 requests per second per connection, plus shared workspace limits. The adapter:

  • Throttles streaming edits with a default minimum interval of 1500ms (streamingEditIntervalMs)
  • Always flushes a final edit
  • Applies a shared limiter to all API calls and honors Retry-After on HTTP 429

Override the interval per stream with updateIntervalMs on the stream options.

Notion limits rich-text spans to about 2000 characters each. The adapter splits long posts into sequential comments to stay under the limit. Streaming edits a single comment in place, so an unusually long streamed response can still approach this ceiling.

Message history

fetchMessages uses Notion's list-comments API (GET /v1/comments?block_id={pageId}), paginated ascending, then filters client-side to the requested discussion_id. The adapter assumes list-comments returns oldest first, because Notion does not document the order. With the SDK default direction: "backward", you get the newest page of matching discussion comments from that assumed order.

List-comments returns only unresolved discussions. Once a discussion is resolved in Notion, its comments disappear from history fetches.

Message subject

await message.subject resolves the parent Notion page (GET /v1/pages/{pageId}) on first access and caches the result.

Requires Capabilities then Content capabilities then Read content. Without it, message.subject returns null, typically after a 403 from the Pages API. Comment posting still works with only the comment capabilities.

FieldValue
type"page"
idPage UUID
titlePage title property (plain text)
urlNotion page URL
status"archived" when the page is archived or in trash; otherwise omitted
authorPage created_by when present
rawFull page API response

The subject is the Notion page object from the Pages API (title, properties, URL, and the full payload on raw), not the page's block children or body content (GET /v1/blocks/.../children). See Message Subject.

Cards and files

Notion has no native card UI. JSX cards render as markdown through fallbackText or flattened content, the same pattern as Linear and GitHub. Interactive buttons and callbackUrl are unsupported.

Outbound files use Notion's File Uploads API and attach up to 3 files as native comment attachments:

  • Binary uploads use single_part, up to Notion's single-part limit of about 20 MiB.
  • Public URLs use external_url and are polled until uploaded before attaching. By default the adapter rechecks immediately, then after 5s and 10s; configure this with externalUrlPollDelaysMs.
  • Files beyond the first 3, uploads still pending after the poll window, and other upload failures fall back to markdown links in the comment body. Edits link files in markdown only.

API version

Every request sends:

Notion-Version: 2026-03-11

That pin is exported as DEFAULT_NOTION_VERSION from @chat-adapter/notion. Override it with notionVersion or NOTION_VERSION only if you accept the risk of breakage when Notion's versioned API diverges.

Thread model

One Notion page is one Chat SDK channel, and each discussion is a thread.

SurfaceThread IDOutbound behavior
Page comment surface (channel root)notion:{pageId}Creates a page-level comment, starting a new page-level discussion.
Existing discussionnotion:{pageId}:{discussionId}Replies in that discussion.
Whole-block discussion (outbound)notion:{pageId}:block:{blockId}, from encodeThreadId({ pageId, blockId }) or the adapter helperStarts a discussion on an entire block via parent.block_id.

Inbound comment.created events always include a discussion_id. The adapter resolves the containing page, including for block-parent comments, and dispatches on notion:{pageId}:{discussionId} so replies stay threaded.

Comment edits and deletions require Read comments to retrieve the target before writing. The adapter throws a ValidationError if the comment is missing or belongs to another page, discussion, or block than the supplied thread ID. For block-parent comments, it retrieves the current block ancestry rather than relying on cached page ownership.

Posts also throw a ValidationError when the thread ID points at another page, and the check runs before any file is uploaded or comment is sent. A whole-block thread ID must name a block on its page, which the adapter confirms from the current block ancestry. Child page and child database blocks are rejected. A discussion thread ID must name a discussion the adapter has recorded for that page. It records a discussion when a comment.created webhook arrives for it, when the bot starts it, or when it finds the discussion in the page's comment list, and keeps the record in the state adapter so every instance shares it. The comment list only covers unresolved, page-level discussions. A post to any other discussion with no record, such as an inline or block discussion whose thread ID was saved before this check existed, throws until a new comment in that discussion arrives through the webhook.

You can start page-level and whole-block discussions through the API. You cannot start selected-text-range (inline highlight) discussions through the public Comments API; those must already exist in Notion.

Use getPageUrl to build deep links:

const notion = bot.getAdapter("notion");
const url = notion.getPageUrl(thread.id); // includes ?d={discussionId} when present

Feature support

Messaging

FeatureSupported
Post message
Message replies
Edit message
Delete message
File uploadsUp to 3 native attachments
StreamingPost+Edit
Scheduled messages

Rich content

FeatureSupported
Card formatMarkdown fallback
Buttons
Link buttons
Select menus
TablesFlattened markdown
Charts
Fields
Images in cards
Modals

Conversations

FeatureSupported
Slash commands
Mentions
Add reactions
Remove reactions
Typing indicator
Mark as read
Message edit events
Message delete events
DMs
Ephemeral messages
User lookup
Parent subject
Native client
Custom API endpoint

Message history

FeatureSupported
Fetch messages
Fetch single message
Fetch thread info
Fetch channel messages
List threads
Fetch channel info
Post channel message

More adapters