npm.io
5.27.0 • Published 6d ago

@notionhq/client

Licence
MIT
Version
5.27.0
Deps
0
Size
1.4 MB
Vulns
0
Weekly
0
Stars
5.7K

Notion SDK for JavaScript

Notion logo

A JavaScript and TypeScript client for the Notion API. This reference covers the SDK's methods, options, and helpers.

Build status npm version

Installation

npm install @notionhq/client

Open Val Town Template

Usage

For setup steps, see Notion's getting started guide.

Client accepts an integration token or an OAuth access token.

const { Client } = require("@notionhq/client")

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
})

Make a request to any Notion API endpoint.

;(async () => {
  const listUsersResponse = await notion.users.list({})
  console.log(listUsersResponse)
})()

See the complete list of endpoints in the API reference.

Request methods return a Promise with the response. For example:

{
  results: [
    {
      object: "user",
      id: "d40e767c-d7af-4b18-a86d-55c61f1e39a4",
      type: "person",
      person: {
        email: "avo@example.org",
      },
      name: "Avocado Lovelace",
      avatar_url:
        "https://secure.notion-static.com/e6a352a8-8381-44d0-a1dc-9ed80e62b53d.jpg",
    },
    // ...
  ]
}

Endpoint parameters are grouped into a single object. You don't need to remember which parameters go in the path, query, or body.

const myPage = await notion.dataSources.query({
  data_source_id: "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
  filter: {
    property: "Landmark",
    rich_text: {
      contains: "Bridge",
    },
  },
})
Handling errors

Notion API errors reject the request with an APIResponseError. The code property identifies the error. APIErrorCode contains the known server error codes.

const {
  Client,
  APIErrorCode,
  isNotionClientError,
} = require("@notionhq/client")

try {
  const notion = new Client({ auth: process.env.NOTION_TOKEN })
  const myPage = await notion.dataSources.query({
    data_source_id: dataSourceId,
    filter: {
      property: "Landmark",
      rich_text: {
        contains: "Bridge",
      },
    },
  })
} catch (error) {
  if (
    isNotionClientError(error) &&
    error.code === APIErrorCode.ObjectNotFound
  ) {
    // Ask the user to select a different data source.
  } else {
    // Other error handling code
    console.error(error)
  }
}
Logging

The default logger writes warnings and errors to the console. LogLevel.DEBUG also logs response bodies.

const { Client, LogLevel } = require("@notionhq/client")

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
  logLevel: LogLevel.DEBUG,
})

A custom logger receives logLevel, message, and extraInfo. It should return no value.

Client options

The Client constructor accepts one options object.

Option Default value Type Description
auth undefined string Bearer token for authentication. If left undefined, the auth parameter should be set on each request.
logLevel LogLevel.WARN LogLevel Verbosity of logs the instance will produce. By default, logs are written to stdout.
timeoutMs DEFAULT_TIMEOUT_MS number Number of milliseconds to wait before emitting a RequestTimeoutError
baseUrl DEFAULT_BASE_URL string The root URL for sending API requests. This can be changed to test with a mock server.
logger Log to console Logger A custom logging function. This function is only called when the client emits a log that is equal or greater severity than logLevel.
agent Default node agent http.Agent Used to control creation of TCP sockets. A common use is to proxy requests with https-proxy-agent
retry See constants RetryOptions Configuration for automatic retries on rate limits (429), service overloads (529), and server errors (500, 503). See Automatic retries below.
dangerouslyAllowBrowser false boolean Allows a token inside a browser page or worker. Without it, the client throws BrowserTokenNotAllowedError there. See Browser usage below.
Browser usage

The Notion API sends CORS headers, so browser code can call api.notion.com directly. The client works in modern browsers with the built-in fetch.

A token in a web page isn't a secret. Anyone who can load the page can read it and act as your connection.

  • For an app other people use, keep the token on a server. Create a public connection, which uses OAuth. Exchange the authorization code on your server, and send the page only the data it needs.
  • For a personal tool or a local page that only you can open, using a token directly is your own risk to accept.

The client blocks tokens in a browser by default. Constructing it with auth inside a browser page throws BrowserTokenNotAllowedError. Pass dangerouslyAllowBrowser: true to confirm you understand the risk.

const notion = new Client({
  auth: tokenTypedIntoYourOwnPage,
  dangerouslyAllowBrowser: true,
})

A token passed per request through auth is blocked the same way: the request rejects with BrowserTokenNotAllowedError before anything is sent. A client without a token still works in a browser, for example with a baseUrl that points at your own server, which adds the token. Web workers and service workers count as a browser too, because every visitor downloads their script. Test runners that emulate a browser, such as jsdom, count as well, so pass dangerouslyAllowBrowser: true in those tests. The agent option is ignored in browsers, and the client doesn't send its user-agent header there, because the API's CORS rules don't allow it. OAuth token endpoints under /v1/oauth/ don't send CORS headers, so exchange authorization codes on a server.

Automatic retries

The client retries failed requests up to 2 times by default. Delays increase with each retry and include a random offset.

Retried errors:

  • rate_limited (HTTP 429) - Too many requests; retried for all HTTP methods
  • service_overload (HTTP 529) - Service overloaded; retried for all HTTP methods
  • internal_server_error (HTTP 500) - Server error; retried only for GET and DELETE
  • service_unavailable (HTTP 503) - Service temporarily unavailable; retried only for GET and DELETE

For server errors, only GET and DELETE are retried to avoid repeating writes. The client uses the Retry-After header when present. It accepts a delay in seconds or an HTTP date.

Retry options:

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
  retry: {
    maxRetries: 5, // Maximum retry attempts (default: 2)
    initialRetryDelayMs: 500, // Initial delay between retries (default: 1000ms)
    maxRetryDelayMs: 60000, // Maximum delay between retries (default: 60000ms)
  },
})

To disable automatic retries:

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
  retry: false,
})
Constants

The SDK exports these defaults and Notion-specific values:

const {
  DEFAULT_BASE_URL, // "https://api.notion.com"
  DEFAULT_TIMEOUT_MS, // 60_000
  DEFAULT_MAX_RETRIES, // 2
  DEFAULT_INITIAL_RETRY_DELAY_MS, // 1_000
  DEFAULT_MAX_RETRY_DELAY_MS, // 60_000
  MIN_VIEW_COLUMN_WIDTH, // 32
} = require("@notionhq/client")

MIN_VIEW_COLUMN_WIDTH is the minimum table column width in pixels. A column at this width appears collapsed. For example:

await notion.views.create({
  database_id: databaseId,
  name: "My view",
  type: "table",
  configuration: {
    table: {
      properties: [
        {
          property_id: checkboxPropId,
          visible: true,
          width: MIN_VIEW_COLUMN_WIDTH,
        },
      ],
    },
  },
})
TypeScript

The package includes types for request parameters, responses, and their fields.

With strict TypeScript, caught errors have type unknown. isNotionClientError narrows the error to a known SDK error type. APIErrorCode identifies server errors; ClientErrorCode identifies errors raised by the client.

import {
  APIErrorCode,
  ClientErrorCode,
  isNotionClientError,
} from "@notionhq/client"

try {
  const response = await notion.dataSources.query({
    data_source_id: dataSourceId,
  })
} catch (error: unknown) {
  if (isNotionClientError(error)) {
    // error is now strongly typed to NotionClientError
    switch (error.code) {
      case ClientErrorCode.RequestTimeout:
        // ...
        break
      case APIErrorCode.ObjectNotFound:
        // ...
        break
      case APIErrorCode.Unauthorized:
        // ...
        break
      default:
        console.error(error)
    }
  }
}
Type guards

These type guards distinguish full API responses from partial responses.

Type guard function Purpose
isFullPage Determine whether an object is a full PageObjectResponse
isFullBlock Determine whether an object is a full BlockObjectResponse
isFullDataSource Determine whether an object is a full DataSourceObjectResponse
isFullPageOrDataSource Determine whether an object is a full PageObjectResponse or DataSourceObjectResponse
isFullUser Determine whether an object is a full UserObjectResponse
isFullComment Determine whether an object is a full CommentObjectResponse

Example:

const fullOrPartialPages = await notion.dataSources.query({
  data_source_id: "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
})
for (const page of fullOrPartialPages.results) {
  if (!isFullPageOrDataSource(page)) {
    continue
  }
  // The page variable has been narrowed from
  //      PageObjectResponse | PartialPageObjectResponse | DataSourceObjectResponse | PartialDataSourceObjectResponse
  // to
  //      PageObjectResponse | DataSourceObjectResponse.
  console.log("Created at:", page.created_time)
}
Utility functions

These helpers read results across multiple pages.

iteratePaginatedAPI(listFn, firstPageArgs)

Returns an async iterator that reads each page of results as needed.

Parameters:

  • listFn: Any function on the Notion client that represents a paginated API (i.e. accepts start_cursor.) Example: notion.blocks.children.list.
  • firstPageArgs: Arguments that should be passed to the API on the first and subsequent calls to the API, for example a block_id.

Returns:

An async iterator over results from the API.

Example:

for await (const block of iteratePaginatedAPI(notion.blocks.children.list, {
  block_id: parentBlockId,
})) {
  // Do something with block.
}
collectPaginatedAPI(listFn, firstPageArgs)

Accepts the same arguments as iteratePaginatedAPI and returns all results in one array. The results must fit in memory.

Parameters:

  • listFn: Any function on the Notion client that represents a paginated API (i.e. accepts start_cursor.) Example: notion.blocks.children.list.
  • firstPageArgs: Arguments that should be passed to the API on the first and subsequent calls to the API, for example a block_id.

Returns:

An array with results from the API.

Example:

const blocks = await collectPaginatedAPI(notion.blocks.children.list, {
  block_id: parentBlockId,
})
// Do something with blocks.
iterateAllDataSourceRows(client, args)

Reads rows beyond the limit for a single data source query, which is 10,000 by default. At that limit, has_more is false and request_status.type is "incomplete"; ordinary pagination stops.

This helper sorts by created_time and starts a new query from the last row's timestamp each time it reaches the limit. It removes duplicate rows by ID.

Parameters:

  • client: A Notion client instance.
  • args: The same arguments as dataSources.query, minus the fields the helper controls: start_cursor (pagination is automatic) and sorts (set to created_time ascending to partition). data_source_id is required. Any filter you pass is combined with the window bound using and.

Returns:

An async iterator over every row in the data source.

Throws:

If a single created_time value holds more rows than the limit, the window cannot be narrowed by time alone. Pass a filter in that case so each window stays under the limit.

Example:

for await (const row of iterateAllDataSourceRows(notion, {
  data_source_id: dataSourceId,
})) {
  // Do something with row.
}
collectAllDataSourceRows(client, args)

Accepts the same arguments as iterateAllDataSourceRows and returns all results in one array. The full data source must fit in memory. For larger data sources, iterateAllDataSourceRows reads rows as a stream.

Parameters:

  • client: A Notion client instance.
  • args: The same arguments as iterateAllDataSourceRows.

Returns:

An array with every row in the data source.

Example:

const rows = await collectAllDataSourceRows(notion, {
  data_source_id: dataSourceId,
})
// Do something with rows.
Custom requests

request() calls a Notion API endpoint directly. For example:

// POST /v1/comments
const response = await notion.request({
  path: "comments",
  method: "post",
  body: {
    parent: { page_id: "5c6a28216bb14a7eb6e1c50111515c3d" },
    rich_text: [{ text: { content: "Hello, world!" } }],
  },
  // No `query` params in this example, only `body`.
})

console.log(JSON.stringify(response, null, 2))

notion.request<ResponseBody>({...}) uses ResponseBody as the expected response type. It does not validate that type at runtime.

Prefer a named SDK method when one exists, such as notion.comments.create() for this example. request() can call endpoints that the SDK does not yet cover.

The Client constructor also accepts a custom fetch function.

Verifying webhook signatures

verifyWebhookSignature checks the signature on a Notion webhook event. It uses the raw request body, the subscription's verification token, and the X-Notion-Signature header. The header contains an HMAC-SHA256 signature in the form sha256=<hex>.

import { verifyWebhookSignature } from "@notionhq/client"

const verificationToken = process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN
if (!verificationToken) {
  throw new Error("Missing webhook verification token")
}

// Express example. Use the raw body: parsing and re-serializing JSON
// can change its bytes and invalidate the signature.
app.post(
  "/notion-webhook",
  express.text({ type: "application/json" }),
  async (req, res) => {
    const body: unknown = req.body
    if (typeof body !== "string") {
      return res.status(400).send("expected a raw text body")
    }
    const ok = await verifyWebhookSignature({
      body,
      signature: req.header("x-notion-signature"),
      verificationToken,
    })
    if (!ok) {
      return res.status(401).send("invalid signature")
    }

    const event: unknown = JSON.parse(body)
    // Validate the event's shape before using its fields.
    res.status(200).send("ok")
  }
)

Subscription setup is separate from event validation. The webhook setup guide covers receiving and saving the initial verification token.

signWebhookPayload({ body, verificationToken }) creates a signature for tests without a live subscription.

Both helpers are async. They use globalThis.crypto.subtle when available and fall back to the Web Crypto API in node:crypto on Node.js.

Examples

For sample code and example projects, see notion-cookbook.

Requirements and compatibility

This package supports the following minimum versions:

  • Runtime: node >= 18
  • Type definitions (optional): typescript >= 5.9

Older versions are not supported. SDK releases also have minimum recommended Notion API versions:

Version of JS/TS SDK Minimum recommended API version
v4.0.0 and above 2022-06-28
v5.0.0 and above 2025-09-03

This SDK supports both 2025-09-03 and 2026-03-11 API versions. The default is 2025-09-03. To use the newer version, pass it when constructing the client:

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
  notionVersion: "2026-03-11",
})

Key changes in 2026-03-11:

  • The position parameter replaces after on appendBlockChildren. It supports after_block, start, and end.
  • The in_trash field replaces archived on pages, blocks, databases, and data sources.
  • The meeting_notes block type replaces transcription.

The SDK's TypeScript types include both sets of field names. Older names are marked @deprecated.

The notionVersion constructor option sets the API version header for requests.

Contributing

Follow the contribution guide to build, test, and check SDK compatibility before opening a pull request.

Endpoint code is generated from a schema in Notion's internal repo. Outside contributors cannot run that generator. Ask a maintainer for generated-code changes; direct edits will be overwritten. Open an issue to discuss changes to the SDK's core behavior first.

Docs, examples, and bug reports are welcome.

Getting help

For API support, SDK problems, or feature requests, email developers@makenotion.com.

You can also open an SDK issue, but we do not monitor issues as closely as email.

Keywords