notion-sdk-js: the official TypeScript client for the Notion API
Official Notion JavaScript Client
At a glance
- What is it?
- The official JavaScript and TypeScript client for the Notion API, published as @notionhq/client. It is a thin typed wrapper over REST, not a sync engine, and its retry policy deliberately refuses to repeat writes on server errors.
- Who is it for?
- Adopt @notionhq/client if you are writing Node 18 or newer code against the Notion API and want typed request objects, automatic retries on 429 and 529, and structured APIResponseError codes instead of hand-rolled fetch calls. Do not adopt it if you need offline caching, webhook handling, or a portable client for browsers and edge runtimes, because only the Node agent and timeout options are documented and no transport abstraction is exposed.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What notion-sdk-js replaces, and who ends up using it
The Notion API is a REST service. Without a client library, every call means assembling a URL, setting the Authorization and Notion-Version headers, serialising a body, and parsing a response by hand. notion-sdk-js removes that layer. The README describes it as "A JavaScript and TypeScript client for the Notion API" and states that it covers the SDK's methods, options, and helpers.
The audience is narrow and specific. You are building a Node service, a script, or an internal tool that reads or writes Notion pages, databases, data sources, users, or views. The package is published as @notionhq/client, and the repository's package.json declares "engines": { "node": ">=18" }, so the floor is Node 18. TypeScript consumers get types from ./build/src/index.d.ts, which is the declared types entry point.
What it is not: a database, a cache, or a synchronisation layer. Every method is a network call. The README's own example is a single await against notion.users.list({}), and that is the whole shape of the library. If you were hoping for local state, change tracking, or offline reads, none of that appears in the documentation.
One options object, one client, and where the parameters go
The Client constructor takes a single options object. The README table lists auth, logLevel, timeoutMs, baseUrl, logger, agent, and retry. Defaults are exported as constants: DEFAULT_BASE_URL is "https://api.notion.com", DEFAULT_TIMEOUT_MS is 60_000, DEFAULT_MAX_RETRIES is 2, DEFAULT_INITIAL_RETRY_DELAY_MS is 1_000, and DEFAULT_MAX_RETRY_DELAY_MS is 60_000.
The design decision worth noticing is parameter flattening. The README states that endpoint parameters are grouped into a single object and that you do not need to remember which parameters go in the path, query, or body. So a data source query takes data_source_id and filter side by side, even though one is a path segment and the other is a body field. That removes a class of mistakes, and it also means the call signature no longer tells you anything about HTTP semantics. If you are debugging against the raw API reference, you have to translate mentally.
Authentication is a bearer token. The README says Client accepts an integration token or an OAuth access token, and that if auth is left undefined, the auth parameter should be set on each request. That second path matters for multi-tenant tools, where one client instance serves several workspaces.
Retries are the other mechanism with real consequences. The client retries rate_limited (429) and service_overload (529) for all HTTP methods, but internal_server_error (500) and service_unavailable (503) only for GET and DELETE, explicitly to avoid repeating writes. It honours the Retry-After header, accepting either a delay in seconds or an HTTP date. Delays increase per attempt with a random offset.
Installing @notionhq/client and making a first query
Installation is one npm command. The README gives it directly:
npm install @notionhq/clientSetup steps live outside the repository. The README points to Notion's getting started guide at developers.notion.com/docs/getting-started for creating the integration and obtaining a token. The SDK does not create integrations for you, and no CLI is shipped that does it.
Once you have a token, construct a client and call an endpoint. The README's first example reads the token from the environment:
const { Client } = require("@notionhq/client")
const notion = new Client({
auth: process.env.NOTION_TOKEN,
})A first real call is users.list, which returns a Promise resolving to the response body. The README shows it wrapped in an async IIFE and logged:
;(async () => {
const listUsersResponse = await notion.users.list({})
console.log(listUsersResponse)
})()The response shape is documented: a results array whose entries carry object, id, type, name, avatar_url, and, for person users, a nested person object with email. If your token is valid and the integration has been added to a workspace, you should see that array populated. An empty or failing response almost always means the token or the workspace connection, not the SDK.
Querying a data source shows the flattened parameter style in practice:
const myPage = await notion.dataSources.query({
data_source_id: "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
filter: {
property: "Landmark",
rich_text: {
contains: "Bridge",
},
},
})For debugging, raise verbosity. LogLevel.DEBUG also logs response bodies, which the default LogLevel.WARN does not:
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, and the README states it should return no value.
Error handling and the retry rules you have to design around
Failed requests reject with an APIResponseError, and the code property identifies the failure. APIErrorCode holds the known server error codes, and isNotionClientError narrows the catch block. The README's example checks for APIErrorCode.ObjectNotFound and suggests asking the user to select a different data source, which is the realistic recovery for that case.
The retry defaults are the part most likely to surprise you. Two retries, an initial delay of 1000ms, and a 60000ms ceiling are reasonable for a read-heavy integration. They are close to useless for a write path that hits 500 or 503, because those statuses are retried only for GET and DELETE. A POST that returns 500 fails immediately. That is a deliberate choice to avoid duplicate writes, and it is the correct default, but it means idempotency is your problem. The SDK gives you no request ID, no de-duplication key, and no transactional wrapper. If you need writes to survive transient server errors, you build that yourself on top.
Two options change behaviour in ways worth testing before production. retry can be set to false to disable automatic retries entirely, and maxRetries, initialRetryDelayMs, and maxRetryDelayMs can be overridden. Raising maxRetries on a write-heavy workload increases the chance of hitting rate limits elsewhere in the same workspace.
Timeouts are separate from retries. timeoutMs controls how long the client waits before emitting a RequestTimeoutError, defaulting to 60_000. A timeout is not a retryable server status, so a slow endpoint produces a hard failure rather than another attempt.
Where the SDK stops: no webhooks, no browser story, no sync
The README documents no webhook support. Notion's event delivery is not part of this client, so any push-based workflow needs a separate HTTP endpoint and its own verification logic. That is a real boundary, not a footnote, because polling the API is the alternative and polling burns rate limit.
The transport is Node-specific. The agent option is typed as http.Agent and the README names https-proxy-agent as the common use, for proxying requests. There is no documented fetch-based or edge-runtime client, and no way to inject a custom transport. If you are deploying to an environment without Node's http module, this package is the wrong tool. The baseUrl option lets you point at a mock server, which is useful for tests, but that is about the destination, not the transport.
There is also no caching, no pagination helper described in the README, and no batching. Each documented method maps to one API endpoint. For a tool that reads a few thousand rows, you will write the pagination loop and the rate-limit backoff strategy yourself, or accept the defaults and let retries absorb 429s.
The versioning constraint is easy to miss. The SDK manages the Notion-Version header internally, but the README does not document how to pin or override it. If your integration depends on a specific API version's response shape, that is something to confirm against the API reference before committing.
notion-sdk-js versus the Python client and raw fetch
The closest alternative is the official Python client, which appears in the related search data as Notion-sdk Python. The two cover the same API surface, so the choice is about your runtime, not capability. If your data pipeline is Python, use the Python client; there is no advantage to shelling out to Node. The JavaScript client's advantage is that it is the one that fits a Node service, a serverless function, or a script in an existing JS codebase, with TypeScript types shipped in the package.
The other alternative is calling the API with fetch or axios directly. That is not unreasonable for a handful of endpoints. You would set the Authorization and Notion-Version headers, build URLs, and handle 429s yourself. What you give up is the exported constants, the APIErrorCode union for narrowing errors, isNotionClientError, and the built-in retry policy with Retry-After parsing. For one endpoint called once a day, hand-rolled fetch is fewer dependencies. For anything that queries data sources, handles errors, and runs unattended, the client earns its place.
A third option, relevant if your problem is really "keep two systems in sync", is a dedicated integration platform. The SDK will not do that for you, and neither will the Python client. Both are request-response clients.
Maintenance, licence, and what upgrading costs
The repository is not archived, and the last push was on 2026-09-16. The most recent release listed is v5.26.0 on 2026-08-31, following v5.25.2 and v5.25.1 on 2026-08-13. The cadence is steady, and the package.json version matches the release tag at 5.26.0.
The licence is MIT, declared in package.json and present as a LICENSE file at the repository root. MIT permits commercial use and modification with the copyright notice retained. That is a statement about the licence text, not legal advice; if your organisation has specific obligations around attribution or distribution, check with whoever handles that.
Upgrade cost is the part to plan for. This is a v5 line, and the API it wraps is versioned separately. A minor SDK bump can still change behaviour if the underlying API version changes, and the README does not document how the client pins that version. The published package contains only build/package.json and build/src/**, so you cannot read the TypeScript source from node_modules; you get compiled output and .d.ts files. The repository does ship a check:compatibility script, but that runs against the build inside the repo, not in your installed copy. The practical approach is to pin the version in package.json, read the release notes before bumping, and keep integration tests that exercise the specific endpoints you call.
Editorial conclusion
Adopt @notionhq/client if you are writing Node 18 or newer code against the Notion API and want typed request objects, automatic retries on 429 and 529, and structured APIResponseError codes instead of hand-rolled fetch calls. Do not adopt it if you need offline caching, webhook handling, or a portable client for browsers and edge runtimes, because only the Node agent and timeout options are documented and no transport abstraction is exposed. Verify two things first: that your integration token is stored outside source control and passed as auth, and that your write paths tolerate the retry rules, since internal_server_error and service_unavailable are retried only for GET and DELETE.
Frequently asked questions
What are the limitations of the Notion API?
The client retries 500 and 503 responses only for GET and DELETE, so failed writes are not retried and idempotency is left to you. It also documents no webhook support, no caching, and no transport other than the Node http agent.
Are Notion APIs free?
The repository does not document pricing or plan requirements for the Notion API. The SDK itself is MIT licensed, but that covers the client code, not access to the service.
What is Notion coded in?
The repository does not describe Notion's own implementation. The SDK repository's primary language is TypeScript, and the published package is compiled with tsc.
Can Notion be used for coding?
The repository does not describe Notion as a coding environment. It documents a JavaScript and TypeScript client for the Notion API, which lets you write code that reads and writes Notion content.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/makenotion-notion-sdk-js)