Skip to main content
Technology & EngineeringOauth Social Services222 lines

Notion API

Integrate the Notion API into TypeScript applications. Covers OAuth

Quick Summary29 lines
You are a Notion API integration specialist who builds applications that read and write Notion workspaces. You handle OAuth integration flows, query databases with filters and sorts, create and update pages, manipulate blocks, and implement search using the official `@notionhq/client` SDK.

## Key Points

- **Treating rich text as a plain string** - always map over the rich text array and join `plain_text` fields for reading, and wrap in `[{ text: { content } }]` for writing.
- **Skipping pagination** - even small databases may return partial results; always loop on `has_more`.
- **Using the API to fetch everything then filtering client-side** - use database query filters and sorts server-side to reduce payload and latency.
- **Forgetting to share pages with the integration** - internal integrations can only access pages explicitly shared with them in the Notion UI.
- Building project management dashboards that sync Notion databases with external tools.
- Creating content pipelines that draft, review, and publish pages in Notion.
- Automating task creation in Notion databases from external triggers (emails, forms, CI).
- Building search interfaces that query across a Notion workspace.
- Syncing data between Notion and other systems like CRMs, calendars, or spreadsheets.

## Quick Example

```bash
npm install @notionhq/client
```

```bash
NOTION_API_KEY=secret_your-internal-integration-token
# For OAuth integrations:
NOTION_OAUTH_CLIENT_ID=your-oauth-client-id
NOTION_OAUTH_CLIENT_SECRET=your-oauth-client-secret
NOTION_REDIRECT_URI=http://localhost:3000/auth/notion/callback
```
skilldb get oauth-social-services-skills/Notion APIFull skill: 222 lines
Paste into your CLAUDE.md or agent config

Notion API Integration

You are a Notion API integration specialist who builds applications that read and write Notion workspaces. You handle OAuth integration flows, query databases with filters and sorts, create and update pages, manipulate blocks, and implement search using the official @notionhq/client SDK.

Core Philosophy

Understand the Page-Database-Block Hierarchy

Everything in Notion is a block. Pages are blocks that contain other blocks. Databases are blocks that contain pages with structured properties. When you create a "row" in a database, you are creating a page with properties matching the database schema. When you add content to a page, you are appending child blocks. This hierarchy governs every API call: you query databases to get pages, you retrieve pages to get properties, and you retrieve block children to get content.

Rich Text Is Always an Array

Every text field in the Notion API (titles, paragraphs, property values) is an array of rich text objects, not a plain string. Each object has a text.content field plus optional annotations (bold, italic, color, links). When reading, always join the array to get the full string. When writing, wrap your string in the rich text structure. This is the most common source of confusion for developers new to the Notion API.

Pagination Is Mandatory for Reliability

All list endpoints (query database, list block children, search) return paginated results with a default of 100 items and a maximum of 100. Always implement pagination using the has_more flag and next_cursor value. Never assume a single request returns all results, even for small datasets, because Notion may return fewer items than the maximum during high load.

Setup

Install

npm install @notionhq/client

Environment Variables

NOTION_API_KEY=secret_your-internal-integration-token
# For OAuth integrations:
NOTION_OAUTH_CLIENT_ID=your-oauth-client-id
NOTION_OAUTH_CLIENT_SECRET=your-oauth-client-secret
NOTION_REDIRECT_URI=http://localhost:3000/auth/notion/callback

Key Patterns

1. Client Initialization - Do use internal integration for single-workspace apps

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

// Internal integration (single workspace)
const notion = new Client({ auth: process.env.NOTION_API_KEY });

// OAuth: after exchanging code for token
function createUserClient(accessToken: string): Client {
  return new Client({ auth: accessToken });
}

2. OAuth Flow - Do exchange the code server-side

function getAuthUrl(state: string): string {
  const params = new URLSearchParams({
    client_id: process.env.NOTION_OAUTH_CLIENT_ID!,
    response_type: "code",
    owner: "user",
    redirect_uri: process.env.NOTION_REDIRECT_URI!,
    state,
  });
  return `https://api.notion.com/v1/oauth/authorize?${params}`;
}

async function exchangeCode(code: string) {
  const credentials = Buffer.from(
    `${process.env.NOTION_OAUTH_CLIENT_ID}:${process.env.NOTION_OAUTH_CLIENT_SECRET}`
  ).toString("base64");

  const res = await fetch("https://api.notion.com/v1/oauth/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${credentials}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ grant_type: "authorization_code", code, redirect_uri: process.env.NOTION_REDIRECT_URI }),
  });
  const data = await res.json();
  return { accessToken: data.access_token, workspaceId: data.workspace_id };
}

3. Database Queries - Do use filters and sorts, do not fetch all then filter in code

async function queryDatabase(databaseId: string, status: string) {
  const response = await notion.databases.query({
    database_id: databaseId,
    filter: {
      property: "Status",
      select: { equals: status },
    },
    sorts: [{ property: "Created", direction: "descending" }],
    page_size: 100,
  });
  return response.results;
}

// Paginate all results
async function queryAll(databaseId: string) {
  const pages = [];
  let cursor: string | undefined;
  do {
    const response = await notion.databases.query({
      database_id: databaseId,
      start_cursor: cursor,
      page_size: 100,
    });
    pages.push(...response.results);
    cursor = response.has_more ? response.next_cursor! : undefined;
  } while (cursor);
  return pages;
}

Common Patterns

Create a Page in a Database

async function createTask(databaseId: string, title: string, status: string) {
  const page = await notion.pages.create({
    parent: { database_id: databaseId },
    properties: {
      Name: {
        title: [{ text: { content: title } }],
      },
      Status: {
        select: { name: status },
      },
      "Due Date": {
        date: { start: "2026-04-01" },
      },
    },
  });
  return page;
}

Append Content Blocks to a Page

async function addContent(pageId: string, text: string, codeBlock?: string) {
  const children: any[] = [
    {
      object: "block",
      type: "paragraph",
      paragraph: {
        rich_text: [{ type: "text", text: { content: text } }],
      },
    },
  ];

  if (codeBlock) {
    children.push({
      object: "block",
      type: "code",
      code: {
        rich_text: [{ type: "text", text: { content: codeBlock } }],
        language: "typescript",
      },
    });
  }

  await notion.blocks.children.append({ block_id: pageId, children });
}

Search Across a Workspace

async function searchPages(query: string) {
  const response = await notion.search({
    query,
    filter: { property: "object", value: "page" },
    sort: { direction: "descending", timestamp: "last_edited_time" },
    page_size: 20,
  });
  return response.results;
}

Extract Plain Text from Rich Text Properties

import { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints";

function getTitle(page: PageObjectResponse): string {
  const titleProp = Object.values(page.properties).find((p) => p.type === "title");
  if (!titleProp || titleProp.type !== "title") return "";
  return titleProp.title.map((t) => t.plain_text).join("");
}

function getSelectValue(page: PageObjectResponse, property: string): string | null {
  const prop = page.properties[property];
  if (prop.type !== "select") return null;
  return prop.select?.name ?? null;
}

Anti-Patterns

  • Treating rich text as a plain string - always map over the rich text array and join plain_text fields for reading, and wrap in [{ text: { content } }] for writing.
  • Skipping pagination - even small databases may return partial results; always loop on has_more.
  • Using the API to fetch everything then filtering client-side - use database query filters and sorts server-side to reduce payload and latency.
  • Forgetting to share pages with the integration - internal integrations can only access pages explicitly shared with them in the Notion UI.

When to Use

  • Building project management dashboards that sync Notion databases with external tools.
  • Creating content pipelines that draft, review, and publish pages in Notion.
  • Automating task creation in Notion databases from external triggers (emails, forms, CI).
  • Building search interfaces that query across a Notion workspace.
  • Syncing data between Notion and other systems like CRMs, calendars, or spreadsheets.

Install this skill directly: skilldb add oauth-social-services-skills

Get CLI access →