How to Use the Notion API

How to Use the Notion API

Published:

Are you getting started connecting to the Notion API? Or are you wondering whether Notion is a good CMS option? This article can serve you as a starting point for querying your content from Notion with Bun or Node JS.

Cover image

I created my new website with Qwik, Bun and Notion. In this stack, Notion serves as a simple CMS. The main benefits that I see in Notion are:

When building my website, I investigated the Notion API. It is simple, and I’m sure that there are many good use cases for using Notion as a data source.

As this article only serves as an entry point, I advise you to look up the reference documentation when you need to go into the API details.

The examples in this post are referring to a basic Kanban database like this:

How is Notion data structured?

In Notion, the three content containers are Databases, Pages, and Blocks.

Databases

Databases are collections of Pages. All Pages within a Database share the same property schema. Pages within a Database can be sorted and filtered.

You can create Views of Databases, but those are inaccessible via the API.

Pages

Pages are objects which have properties. Those properties have a name, a type, and a value. Pages also have some meta attributes like the last change date, an icon, and a cover.

A Page can be nested within another Page or a Database.

A Page’s content is a list of child blocks.

Blocks

There are various types of blocks, like paragraphs, page links and images. They are part of a page or another block.

How to gain access to the API?

To connect to the Notion API, you first need an API key. You can create one in Settings & members Connections Develop or manage integrations . Here you can customize your Integration with a name, an image and the capabilities.

The so-called integration token is what we need to use later in our queries.

Now it’s time to add your new Connection to the Pages you want to access. The connection is passed down the page hierarchy, so you normally only need to do this on one or a few top-level pages:

On the page, click the three dots in the top-right corner. Then pick Add connections [Connection name] .

Now we are ready to go.

Querying the API

Now we have an API token and a basic overview of the content structure in Notion, we can start querying the API.

You can either directly use the HTTP API, or the npm package @notionhq/client .

To get started, let’s try to fetch some information about a page. Make sure, you have created a page in Notion, which has the Connection attached, we created before.

You can find the page’s ID as part of the URL in your browser. The URL schema of a page is https://notion.so/[page name slug]-[page id] .

Using fetch

First, let’s forge a request to the API directly with fetch .

const res = await fetch(
  `https://api.notion.com/v1/pages/${PAGE_ID}`,
  {
    headers: {
      "Notion-Version": "2022-06-28",
      Authorization: `Bearer ${API_KEY}`,
    },
  },
);

const page = await res.json();

console.log(page);

This will get us the page information.

Using the Notion Client

However, we can also use the Notion client to get the same information.

First, install it via bun add @notionhq/client to your project. Now we are ready to go.

import { Client } from "@notionhq/client";

const client = new Client({ auth: API_KEY });

const page = await client.pages.retrieve({
  page_id: PAGE_ID,
});

console.log(page);

Fetch vs. Notion Client

So, we can either use the REST API directly or use the Notion client abstraction. Which of both is the better way?

The advantage of using isomorphic fetch is that you don’t introduce additional dependencies. I am always cautious when it comes to external dependencies. I always ask myself whether they are worth the payload. And every dependency may bring in potential security vulnerabilities.

That said, @notionhq/client depends solely on node-fetch , which comes only with a handful of dependencies. There is already a github issue , which proposes to use isomorphic fetch .

However, there is some value in using the Notion client. The things that I find really useful are:

In the rest of this article, we will use the Notion client.

API endpoints

Covering the whole API would be out-of-scope of this introduction. We will fetch some pages and blocks, for the rest of the API, you can visit the docs .

Page API

We already saw how to retrieve a page from the API:

const page = await client.pages.retrieve({
  page_id: PAGE_ID,
});

Let’s take a closer look at the response. To get a concise overview, We’ll use excerpts of the corresponding typings from @notionhq/client :

type PageObjectResponse = {
  parent: ...;
  properties: ...;
  icon: ...;
  cover: ...;
  created_by: PartialUserObjectResponse;
  last_edited_by: PartialUserObjectResponse;
  object: "page";
  id: string;
  created_time: string;
  last_edited_time: string;
  archived: boolean;
  url: string;
  public_url: string | null;
}

In this page object, we see all the information that are related to the page itself, including our custom properties.

Blocks API

What is missing from the page? You are right, it’s content. If we want to get this page’s child blocks, we will have to send a separate request.

const {results: blocks} = await client.blocks.children.list({
  block_id: PAGE_ID,
});

This will return us a paginated response of some blocks.

type ListBlockChildrenResponse = {
  type: "block";
  block: EmptyObject;
  object: "list";
  next_cursor: string | null;
  has_more: boolean;
  results: Array<BlockObjectResponse>;
}
type BlockObjectResponse = ParagraphBlockObjectResponse | ...
type ParagraphBlockObjectResponse = {
  type: "paragraph";
  paragraph: ...;
  parent: ...;
  object: "block";
  id: string;
  created_time: string;
  created_by: PartialUserObjectResponse;
  last_edited_time: string;
  last_edited_by: PartialUserObjectResponse;
  has_children: boolean;
  archived: boolean;
};

We have a paginated API here, so we only get the first page with some pagination information. We will see how to efficiently process paginated APIs later.

There are various types of blocks in Notion. For simplicity, I only listed the paragraph. From the API, we get interesting meta information about those blocks, as well as their content (nested in the [block-type] property).

Database API

We can query the pages within the database like this:

const { results: pages } = await client.databases.query({
  database_id: DATABASE_ID,
});

If we want to limit those to the most recent changes, we can forge a query with a matching filter.

const { results: pages } = await client.databases.query({
  database_id: DATABASE_ID,
  filter: {
		timestamp: "last_edited_time",
    last_edited_time: {this_week: {}},
  },
});

Within a database, all pages share a set of properties, which are defined by the database schema. We can use those to filter and sort the pages.

const { results: pages } = await client.databases.query({
  database_id: DATABASE_ID!,
  sorts: [
    {
      property: "Status",
      direction: "ascending",
    },
    {
      timestamp: "last_edited_time",
      direction: "descending",
    },
  ],
  filter: {
    and: [
      {
        timestamp: "last_edited_time",
        last_edited_time: { this_week: {} },
      },
      {
        or: [
          {
            property: "Status",
            select: { equals: "Done" },
          },
          {
            property: "Status",
            select: { equals: "In progress" },
          },
        ],
      },
    ],
  },
});

As we can see, we can build quite complex filter queries. The one above will list all pages edited this week and having the property Status set to "Done" or "In progress" . The results are sorted by Status and then by last edited time.

A database query response looks like this:

type QueryDatabaseResponse = {
  type: "page_or_database";
  page_or_database: EmptyObject;
  object: "list";
  next_cursor: string | null;
  has_more: boolean;
  results: Array<PageObjectResponse | DatabaseObjectResponse>;
};

As we can tell by the next_cursor and has_more properties, the database query endpoint, like the child blocks endpoint, is a paginated API.

Good to know

Handling paginated APIs

If we want to paginate through the API, we can either do this manually, or use one of two helper functions from the Notion client.

The collectPaginatedAPI() helper will iterate a paginated API and return a Promise of a list of results.

const allBlocks = await collectPaginatedAPI(
  client.blocks.children.list,
  { block_id: PAGE_ID },
);
// Do something with all blocks

If you wish to save memory, you can also iterate the paginated API with the iteratePaginatedAPI() helper. It is an async generator that lets you visit every single item.

for await (const block of iteratePaginatedAPI(
  client.blocks.children.list,
  { block_id: PAGE_ID },
)) {
  // Do something with each block
}

Rate limiting

The Notion API is rate-limited per integration. At the time of writing, there may be a max average of 3 requests per second, but short bursts of multiple requests are allowed. In their docs about rate limiting, they state that Notion may change this in the future, to be plan-dependent.

This is important to keep in mind, and this rate limit also limits the use cases for apps using the Notion API.

Conclusion

We only scratched the surface here.

You can also create, update and delete pages, blocks, and even whole databases via the API.

We did not touch the entities User and Comment . Those may be useful in some situations as well.

As we saw, the Notion API is well-equipped to serve as a content data source.

Maybe, you already have new ideas sparking up, what you could build with it.

I wish you happy coding and have a nice day!