Notion SDK for JavaScript
A JavaScript and TypeScript client for the Notion API. This reference covers the SDK's methods, options, and helpers.
Installation
npm install @notionhq/client
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 methodsservice_overload(HTTP 529) - Service overloaded; retried for all HTTP methodsinternal_server_error(HTTP 500) - Server error; retried only for GET and DELETEservice_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. acceptsstart_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 ablock_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. acceptsstart_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 ablock_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 asdataSources.query, minus the fields the helper controls:start_cursor(pagination is automatic) andsorts(set tocreated_timeascending to partition).data_source_idis required. Anyfilteryou pass is combined with the window bound usingand.
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 asiterateAllDataSourceRows.
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.subtlewhen available and fall back to the Web Crypto API innode:cryptoon 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
positionparameter replacesafteronappendBlockChildren. It supportsafter_block,start, andend. - The
in_trashfield replacesarchivedon pages, blocks, databases, and data sources. - The
meeting_notesblock type replacestranscription.
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.