# Notion SDK for JavaScript
A JavaScript and TypeScript client for the [Notion API](https://developers.notion.com). This reference covers the SDK's methods, options, and helpers.

[](https://www.npmjs.com/package/@notionhq/client)
## Installation
```bash
npm install @notionhq/client
```
[](https://www.val.town/v/charmaine/NotionJsSDK)
## Usage
> [!NOTE]
> For setup steps, see Notion's [getting started guide](https://developers.notion.com/docs/getting-started).
`Client` accepts an integration token or an OAuth access token.
```js
const { Client } = require("@notionhq/client")
const notion = new Client({
auth: process.env.NOTION_TOKEN,
})
```
Make a request to any Notion API endpoint.
```js
;(async () => {
const listUsersResponse = await notion.users.list({})
console.log(listUsersResponse)
})()
```
> [!NOTE]
> See the complete list of endpoints in the [API reference](https://developers.notion.com/reference).
Request methods return a `Promise` with the response. For example:
```ts
{
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.
```js
const myPage = await notion.dataSources.query({
data_source_id: "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
filter: {
property: "Landmark",
rich_text: {
contains: "Bridge",
},
},
})
```
### Markdown versions
Enhanced Markdown (`v1`) is deprecated. API versions through `2026-03-11` still default to `v1`. The next API version will default to `v2` and reject requests that specify `v1`. Notion plans to retire `v1` on older API versions about three months after that release.
Set `markdown_version: "v2"` on Markdown reads and writes now to test your integration before you upgrade. [Migrate to Notion-flavored Markdown](https://developers.notion.com/guides/data-apis/migrate-to-notion-flavored-markdown)
```js
const { markdown } = await notion.pages.retrieveMarkdown({
page_id: pageId,
markdown_version: "v2",
})
```
### Per-request beta flags
Pass `betas` to a typed endpoint to opt that call into one or more API betas. The SDK sends the flags in one comma-separated `Notion-Beta` header. Omitting `betas` or passing an empty array sends no beta header.
```js
await notion.dataSources.query({
data_source_id: dataSourceId,
betas: ["status-group-filters-2026-10-06"],
filter: { property: "Status", status: { group_equals: "In progress" } },
})
```
This query matches every status option in the “In progress” group. [Status group filters beta](https://developers.notion.com/reference/filter-data-source-entries#status-group-filters-beta)
TypeScript suggests the current flags known to this SDK. You can also pass a newer flag by its full name and revision date. The API checks whether each flag is valid. [Beta versions](https://developers.notion.com/reference/versioning#beta-versions)
### Handling errors
Notion API errors reject the request with an `APIResponseError`. The `code` property identifies the error. `APIErrorCode` contains the known server error codes.
```js
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.
```js
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`](https://github.com/TooTallNate/node-https-proxy-agent) |
| `retry` | See [constants](#constants) | `RetryOptions` | Configuration for automatic retries on rate limits (429), service overloads (529), and server errors (500, 503). See [Automatic retries](#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](#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](https://developers.notion.com/guides/get-started/public-connections), 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.
```js
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 in a browser count as a browser too, because every visitor downloads their script. Server runtimes that borrow worker APIs, such as Cloudflare Workers and Deno worker threads, don't count. 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, so a proxy's CORS rules don't need to 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:
```js
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:
```js
const notion = new Client({
auth: process.env.NOTION_TOKEN,
retry: false,
})
```
### Constants
The SDK exports these defaults and Notion-specific values:
```js
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:
```js
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.
```ts
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](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types)
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:
```typescript
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
over results from the API.
Example:
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
const rows = await collectAllDataSourceRows(notion, {
data_source_id: dataSourceId,
})
// Do something with rows.
```
### Custom requests
`request()` calls a Notion API endpoint directly. For example:
```ts
// 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({...})` uses `ResponseBody` as the expected response type. It does not validate that type at runtime.
Use `formDataParams` for endpoints that take a file. For example, to upload a skill archive to a page in a Skills database:
```ts
import { readFile } from "fs/promises"
import type { UploadSkillResponse } from "@notionhq/client"
// POST /v1/ai/skills/:id/upload
const response = await notion.request({
path: `ai/skills/${skillPageId}/upload`,
method: "post",
formDataParams: {
file: {
filename: "skill.tar.gz",
data: new Blob([await readFile("./skill.tar.gz")], {
type: "application/gzip",
}),
},
},
})
```
> [!TIP]
> 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](https://developers.notion.com/reference/webhooks). 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=`.
```ts
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](https://developers.notion.com/reference/webhooks#step-2-verifying-the-subscription) covers receiving and saving the initial verification token.
`signWebhookPayload({ body, verificationToken })` creates a signature for tests without a live subscription.
> [!NOTE]
> Both helpers are async and use `globalThis.crypto.subtle`.
## Examples
For sample code and example projects, see [notion-cookbook](https://github.com/makenotion/notion-cookbook/tree/main/examples).
## Requirements and compatibility
This package supports the following minimum versions:
- Runtime: `node >= 22`
- Type definitions (optional): `typescript >= 5.9`
Older versions are not supported. SDK releases also have minimum recommended [Notion API versions](https://developers.notion.com/reference/versioning):
| 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:
```js
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](CONTRIBUTING.md) 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](https://github.com/makenotion/notion-sdk-js/issues), but we do not monitor issues as closely as email.