Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Notion API 入門と実践ガイド

Notion API を使って Python からページやデータベースを操作するための実践的な技術解説書です。日々のタスク管理の自動化、外部サービスとのデータ連携、独自ツールの構築に役立つ知識を段階的に学べます。

本書の目的

本書は、Notion API の基礎知識から Python による実践的なプログラミング手順までを、初学者にもわかりやすく解説することを目的としています。 公式 SDK である notion-client および汎用 HTTP ライブラリの requests の両方に対応し、用途に応じた最適な実装方法を習得できます。

章構成

第1章 Notion APIの概要と仕組み

Notion API の基本的な役割、REST API としての特徴、利用できる機能と制限について解説します。

Notion APIとは

Notion API は、多機能メモ・ドキュメントツールである Notion を、外部のプログラムから操作するための窓口(インターフェース)です。 Notion の画面を手作業でクリックして行っている作業を、プログラムコードによって自動化できます。

たとえば、毎日決まった時間にタスクを自動作成したり、Web サイトで集めたデータを Notion の表(データベース)に自動登録したり、Notion 内のメモを別のサービスに転送したりすることが可能です。

REST APIとしての特徴

Notion API は、標準的な Web 技術である REST(Representational State Transfer) に基づいて作られています。 HTTP 通信を利用して、世界中どこからでも決まった形式でリクエストを送り、JSON 形式でデータを受け取ります。

  • エンドポイント URL: 基本となる URL は https://api.notion.com/v1/ です。
  • データ形式: 送信データおよび受信データはすべて JSON(JavaScript Object Notation) 形式で表現されます。
  • HTTP メソッド: 取得には GET、新規作成には POST、部分更新には PATCH、削除には DELETE を使用します。

Notion APIでできること・できないこと

Notion API を使うことで多くの操作が可能になりますが、セキュリティやシステム設計上の理由から、一部の操作には制限があります。

できること

  • データベースの作成、スキーマ(列定義)の変更、行データの検索(クエリ)
  • ページの新規作成、タイトルやプロパティの更新、ページのアーカイブ(削除)
  • ページ内の本文ブロック(見出し、段落、リスト、画像など)の追加と更新
  • ワークスペース内のコンテンツをキーワードで横断検索
  • ワークスペース内のユーザー一覧や自分(Bot)の情報取得

できないこと(または制限があること)

  • ページの完全削除(ゴミ箱を空にする操作):API からはアーカイブ状態(ゴミ箱へ移動)にする操作のみ可能です。
  • ワークスペース自体の設定変更やメンバーの招待・削除
  • Notion AI の直接操作やプロンプト実行
  • 1 秒あたりにおよそ 3 回を超える高頻度の連続リクエスト(レートリミットによる制限)

APIバージョン管理

Notion API では、後方互換性を保ちながら改善を続けるために、リクエストヘッダーに API バージョン(日付表記)を指定する仕組みを採用しています。

Notion-Version: 2022-06-28

バージョンを指定することで、Notion 側の機能追加や仕様変更があっても、開発したプログラムが突然動かなくなるトラブルを防げます。 Python の公式 SDK(notion-client)を使用する場合は、適切なバージョンが自動的に設定されます。

第2章 開発環境のセットアップと認証

Notion API を利用するためのインテグレーション作成、認証トークンの取得、対象ページへのアクセス権限の付与、および Python 開発環境の構築手順を解説します。

1. インテグレーションの作成

Notion API を利用するには、まず Notion の開発者ポータルで インテグレーション(Integration) を登録する必要があります。 インテグレーションとは、外部プログラムと Notion ワークスペースを仲介する専用アカウントのようなものです。

  1. Web ブラウザで Notion Developers にアクセスします。
  2. 画面右上の「新しいインテグレーション(New integration)」ボタンをクリックします。
  3. 基本情報を入力します:
    • 名前: プログラムの目的がわかる名前(例: My Notion Bot)を入力します。
    • 関連付けられたワークスペース: API を利用したいワークスペースを選択します。
    • 機能(Capabilities): コンテンツの読み取り、更新、挿入に必要な権限にチェックを入れます。
  4. 「保存」をクリックすると、インテグレーションが作成されます。

2. 内部インテグレーションシークレットの取得

インテグレーションが作成されると、「シークレット」タブに 内部インテグレーションシークレット(Internal Integration Secret) が表示されます。 この文字列は secret_... または ntn_... から始まります。

このシークレットは、Notion に「誰からのリクエストか」を証明するパスワードのような重要情報です。 絶対に他人に教えたり、GitHub などの公開リポジトリにコミットしたりしないでください。

3. ページやデータベースへの接続権限の付与

インテグレーションを作成しただけでは、ワークスペース内のデータには一切アクセスできません。 プログラムから操作したいページやデータベースごとに、個別にアクセスを許可(コネクトを追加)する必要があります。

  1. Notion アプリまたはブラウザで、操作対象のページまたはデータベースを開きます。
  2. 画面右上の「…」(その他のオプション)メニューを開きます。
  3. メニュー下部の「接続(Connect to)」または「コネクト」を選択します。
  4. 作成したインテグレーション名(例: My Notion Bot)を検索して選択し、アクセスを許可します。

親ページへのアクセスを許可すると、そのページ配下にある子ページやデータベースにも自動的に権限が継承されます。

4. ページIDとデータベースIDの確認方法

API からページやデータベースを特定するには、32 桁の 16 進数で構成される一意の ID を指定します。 ID は、ブラウザで対象のページを開いた際の URL から簡単に取得できます。

https://www.notion.so/myworkspace/My-Database-Title-a1b2c3d4e5f67890123456789abcdef0?v=...

URL 内のタイトル名に続くハイフン後の 32 文字(上記例では a1b2c3d4e5f67890123456789abcdef0)が ID です。 クエリパラメータ(?v=... 以降)は不要です。ハイフンが含まれている形式でも、ハイフンなしの形式でも、どちらでも API に指定可能です。

5. Python環境のセットアップ

Python で Notion API を操作するために、必要なライブラリをインストールします。

pip install notion-client requests python-dotenv
  • notion-client: Notion 公式が提供する Python SDK です。API の呼び出しや型ヒント、自動エラーハンドリングが組み込まれています。
  • requests: Python で最も広く使われている HTTP 通信ライブラリです。SDK を使わずに生の REST API を直接操作する場合に利用します。
  • python-dotenv: .env ファイルに記述した環境変数を Python コードから読み込むためのライブラリです。

6. 認証情報の安全な管理

シークレットキーをプログラムコードに直接書き込むのは危険です。 作業ディレクトリ内に .env ファイルを作成し、環境変数として管理します。

NOTION_TOKEN=secret_your_actual_integration_secret_here
NOTION_DATABASE_ID=a1b2c3d4e5f67890123456789abcdef0

.gitignore ファイルに .env を追加して、Git の管理対象から除外することを忘れないようにしましょう。

7. 疎通確認スクリプト

環境変数を読み込んで、Notion API と正常に通信できるかを確認する簡単な Python スクリプトを作成します。

import os
from dotenv import load_dotenv
from notion_client import Client

# .env ファイルから環境変数を読み込む
load_dotenv()

notion_token = os.getenv("NOTION_TOKEN")
if not notion_token:
    raise ValueError("NOTION_TOKEN が設定されていません。")

# クライアントの初期化
notion = Client(auth=notion_token)

# 自身のインテグレーション(Bot)情報を取得
bot_user = notion.users.me()
print("接続成功!")
print(f"Bot 名: {bot_user.get('name')}")
print(f"Bot ID: {bot_user.get('id')}")

このスクリプトを実行し、「接続成功!」と Bot 名が表示されれば、環境構築と認証設定は完了です。

第3章 Notionのデータモデルと基本構造

Notion API を使いこなすために最も重要な「データ構造」の基礎概念を解説します。ページ、データベース、ブロック、プロパティの関係を整理し、JSON レスポンスの読み解き方を学びます。

Notion の基本オブジェクト階層

Notion のコンテンツは、すべて「ブロック」と呼ばれる積み木のような単位で構成されています。 全体の関係は以下の階層構造になっています。

Workspace(ワークスペース)
 ├── Database(データベース:構造化された表やリスト)
 │    └── Page(ページ:データベースの各行・レコード)
 │         └── Block(ブロック:見出し、本文段落、箇条書きなど)
 └── Page(単体の独立ページ)
      └── Block(ブロック)
           └── Child Blocks(子ブロック)

重要な点は、「データベースの各レコード(行)も、実体は 1 つの独立したページである」 という点です。 行をクリックすると詳細ページが開く Notion の特徴が、そのまま API のデータ構造にも反映されています。

ブロック(Block)の仕組み

Notion のドキュメント本文は、1 行 1 行が独立した ブロック として管理されます。 段落(paragraph)、見出し(heading_1, heading_2, heading_3)、チェックボックス(to_do)、箇条書き(bulleted_list_item)、コード(code)など、多彩なタイプが存在します。

ブロックは親子関係(入れ子構造)を持つことができます。たとえば、トグルリストや箇条書きのインデント行は、親ブロックの children として子ブロックがぶら下がります。

ブロックの JSON 表現例(段落ブロック)

{
  "object": "block",
  "id": "c1a2b3c4-...",
  "type": "paragraph",
  "paragraph": {
    "rich_text": [
      {
        "type": "text",
        "text": {
          "content": "こんにちは、Notion API!"
        },
        "plain_text": "こんにちは、Notion API!"
      }
    ]
  }
}

リッチテキストオブジェクト(Rich Text Object)

Notion のテキスト情報は、単純な文字列ではなく Rich Text Object の配列として表現されます。 文章の一部分だけを太字にしたり、リンクを設定したり、文字色を変えたりできるのは、この構造のおかげです。

1 つのリッチテキスト要素には、主に以下のキーが含まれます:

  • type: 通常は "text"(メンションや数式の場合は "mention", "equation")
  • text.content: 表示されるテキスト本文
  • text.link: リンク先 URL(設定されている場合)
  • annotations: 文字装飾(bold, italic, strikethrough, underline, code, color)
  • plain_text: 装飾を除去したプレーンテキスト

Python でテキストを抽出するときは、item["plain_text"] を参照すると装飾を気にせず文字列を取得できます。

データベースのプロパティ(Properties)

データベースの各列(属性)を プロパティ と呼びます。 プロパティには厳密な型があり、型によって JSON の書き方が異なります。

プロパティ型説明JSON 値の指定方法の例
titleレコードの名前(1つのDBに必須で1列のみ){"title": [{"text": {"content": "タイトル"}}]}
rich_text複数行や装飾が可能なテキスト列{"rich_text": [{"text": {"content": "メモ"}}]}
number数値(整数・小数){"number": 42}
select単一選択ドロップダウン{"select": {"name": "進行中"}}
multi_select複数選択タグ{"multi_select": [{"name": "Python"}, {"name": "API"}]}
date日付(開始日・終了日・時刻){"date": {"start": "2026-10-03"}}
checkboxチェックボックス(True/False){"checkbox": true}
statusステータス列(Not started, In progress等){"status": {"name": "完了"}}
urlWeb サイトのリンク{"url": "https://example.com"}

データの取得と更新のルール

プロパティの値を更新する場合、指定するキー名は Notion 上の プロパティ名 または プロパティID と一致している必要があります。 たとえば列名が「担当者」であれば、キー名にも "担当者" と指定します。

また、title プロパティは各データベースに必ず 1 列だけ存在し、行を識別するメインの見出しとして機能します。

第4章 データベースのクエリとレコード操作

Notion のデータベースを対象に、レコードの検索(クエリ)、条件絞り込み(フィルター)、並び替え(ソート)、新規レコードの挿入、既存レコードの更新を行う Python プログラムの実装方法を解説します。

データベースのレコード取得(クエリ)

データベースの全レコードまたは特定条件に合うレコードを取得するには、databases.query を呼び出します。

Python(notion-client)による基本クエリ

import os
from dotenv import load_dotenv
from notion_client import Client

load_dotenv()
notion = Client(auth=os.getenv("NOTION_TOKEN"))
database_id = os.getenv("NOTION_DATABASE_ID")

# データベースからレコードを取得
response = notion.databases.query(
    database_id=database_id,
    page_size=10
)

# レコード一覧をループ処理
for page in response.get("results", []):
    page_id = page["id"]
    props = page["properties"]
    
    # "名前" 列(title型)の値を取得
    title_list = props.get("名前", {}).get("title", [])
    title_text = title_list[0]["plain_text"] if title_list else "(無題)"
    
    print(f"ID: {page_id} | タイトル: {title_text}")

フィルターとソートの指定

Notion API では、条件を指定して効率的にデータを絞り込むことができます。

フィルター(絞り込み)

たとえば、「ステータスが “未着手”」または「期限が今日以降」のタスクを絞り込む構文です。

# ステータスが「未着手」のレコードのみ取得
query_filter = {
    "property": "ステータス",
    "status": {
        "equals": "未着手"
    }
}

response = notion.databases.query(
    database_id=database_id,
    filter=query_filter
)

複合条件(AND / OR)を指定する場合は、and や or のリストを使います。

query_filter = {
    "and": [
        {
            "property": "ステータス",
            "status": {
                "equals": "進行中"
            }
        },
        {
            "property": "優先度",
            "select": {
                "equals": "高"
            }
        }
    ]
}

ソート(並び替え)

取得結果を特定の日付順や優先度順に並べ替えるには sorts パラメータを指定します。

response = notion.databases.query(
    database_id=database_id,
    sorts=[
        {
            "property": "作成日時",
            "direction": "descending"  # 降順(新しい順)
        }
    ]
)

ページネーション(100件を超えるデータの取得)

Notion API は、1 回のリクエストで最大 100 件のレコードしか返しません。 100 件を超えるデータをすべて取得するには、カーソルを使ったページネーション処理を行います。

all_records = []
has_more = True
next_cursor = None

while has_more:
    kwargs = {"database_id": database_id, "page_size": 100}
    if next_cursor:
        kwargs["start_cursor"] = next_cursor
        
    res = notion.databases.query(**kwargs)
    all_records.extend(res.get("results", []))
    
    has_more = res.get("has_more", False)
    next_cursor = res.get("next_cursor")

print(f"取得完了: 合計 {len(all_records)} 件")

新規レコード(行)の追加

データベースに新しい行を追加するには、親(parent)にデータベース ID を指定して ページ作成 API(pages.create) を呼び出します。

new_page = notion.pages.create(
    parent={"database_id": database_id},
    properties={
        "タスク名": {
            "title": [
                {"text": {"content": "週次レポート作成"}}
            ]
        },
        "ステータス": {
            "status": {
                "name": "未着手"
            }
        },
        "優先度": {
            "select": {
                "name": "高"
            }
        },
        "期日": {
            "date": {
                "start": "2026-10-10"
            }
        },
        "完了": {
            "checkbox": False
        }
    }
)

print(f"新規レコード作成完了: {new_page['id']}")

既存レコードのプロパティ更新

既存のレコード(ページ)のプロパティを変更するには、pages.update を使用します。変更したい列のみを指定します。

page_id = "target_page_id_here"

updated_page = notion.pages.update(
    page_id=page_id,
    properties={
        "ステータス": {
            "status": {
                "name": "完了"
            }
        },
        "完了": {
            "checkbox": True
        }
    }
)

print(f"レコード更新完了: {updated_page['id']}")

第5章 ページとブロックの作成・更新

Notion のページ作成、本文ブロック(見出し、段落、リスト、コードなど)の追加・読み取り・更新・削除を行う Python プログラムの実装方法を解説します。

単体ページの新規作成

データベースではなく、既存のページの下に新しい子ページを作成する場合は、parent に親ページの ID(page_id)を指定します。

import os
from dotenv import load_dotenv
from notion_client import Client

load_dotenv()
notion = Client(auth=os.getenv("NOTION_TOKEN"))
parent_page_id = "parent_page_id_here"

# 子ページの作成(タイトルのみ)
new_page = notion.pages.create(
    parent={"page_id": parent_page_id},
    properties={
        "title": [
            {"text": {"content": "プロジェクト設計メモ"}}
        ]
    }
)

print(f"ページ作成成功: {new_page['id']}")

ページ本文にブロックを追加する

ページ作成時に、本文となる子ブロック(children)を一度にまとめて挿入できます。 各ブロックは type とその中身を定義した辞書で構成されます。

# 見出し、段落、ToDo、コードブロックを含むページを作成
page_with_content = notion.pages.create(
    parent={"page_id": parent_page_id},
    properties={
        "title": [{"text": {"content": "API 実装ガイド"}}]
    },
    children=[
        {
            "object": "block",
            "type": "heading_2",
            "heading_2": {
                "rich_text": [{"type": "text", "text": {"content": "はじめに"}}]
            }
        },
        {
            "object": "block",
            "type": "paragraph",
            "paragraph": {
                "rich_text": [
                    {"type": "text", "text": {"content": "これは Python スクリプトから自動生成されたページです。"}}
                ]
            }
        },
        {
            "object": "block",
            "type": "to_do",
            "to_do": {
                "rich_text": [{"type": "text", "text": {"content": "API トークンを設定する"}}],
                "checked": True
            }
        },
        {
            "object": "block",
            "type": "code",
            "code": {
                "language": "python",
                "rich_text": [{"type": "text", "text": {"content": "print('Hello Notion')\n"}}]
            }
        }
    ]
)

既存ページにブロックを追記する

すでに存在するページに後からブロックを追加する場合は、blocks.children.append を使用します。

page_id = "target_page_id_here"

notion.blocks.children.append(
    block_id=page_id,
    children=[
        {
            "object": "block",
            "type": "paragraph",
            "paragraph": {
                "rich_text": [{"type": "text", "text": {"content": "追記されたメモです。"}}]
            }
        }
    ]
)

ページ内のブロックを読み取る

ページにどんなブロックが配置されているかを取得するには、blocks.children.list を呼び出します。 ブロックも 1 回のリクエストで最大 100 件まで返されるため、必要に応じてページネーションを行います。

page_id = "target_page_id_here"

response = notion.blocks.children.list(block_id=page_id)

for block in response.get("results", []):
    b_type = block["type"]
    b_id = block["id"]
    
    # 段落の場合のテキスト取り出し
    if b_type == "paragraph":
        texts = [t["plain_text"] for t in block["paragraph"]["rich_text"]]
        print(f"[段落] {''.join(texts)}")
    elif b_type == "to_do":
        texts = [t["plain_text"] for t in block["to_do"]["rich_text"]]
        checked = block["to_do"]["checked"]
        mark = "[x]" if checked else "[ ]"
        print(f"[ToDo] {mark} {''.join(texts)}")

ブロックの更新と削除

ブロック内容の更新

既存のブロックの内容を書き換えるには blocks.update を使います。

block_id = "target_block_id_here"

notion.blocks.update(
    block_id=block_id,
    paragraph={
        "rich_text": [{"type": "text", "text": {"content": "内容を更新しました。"}}]
    }
)

ブロックの削除

不要になったブロックは blocks.delete で削除(ゴミ箱へ移動)します。

notion.blocks.delete(block_id=block_id)

ページのアーカイブ(ゴミ箱移動)

ページ全体を削除(アーカイブ)したい場合は、pages.update で archived=True を指定します。

notion.pages.update(
    page_id=page_id,
    archived=True
)
print("ページをアーカイブしました。")

第6章 ワークスペース検索とユーザー情報取得

Notion ワークスペース全体をキーワードで横断検索する Search API と、ワークスペース内のメンバー情報や Bot アカウントの情報を取得する Users API の活用法を解説します。

Search API によるワークスペース横断検索

インテグレーションが共有されているページやデータベースの中から、タイトルに含まれるキーワードで検索できます。 ID がわからないページを動的に探す際などに大変便利です。

キーワード検索の基本

import os
from dotenv import load_dotenv
from notion_client import Client

load_dotenv()
notion = Client(auth=os.getenv("NOTION_TOKEN"))

# "会議" というキーワードを含むページやデータベースを検索
search_results = notion.search(
    query="会議",
    page_size=5
)

for result in search_results.get("results", []):
    obj_type = result["object"]  # "page" または "database"
    item_id = result["id"]
    
    # ページとデータベースでタイトルの格納場所が異なる点に注意
    if obj_type == "page":
        props = result.get("properties", {})
        title_prop = props.get("title") or props.get("名前") or {}
        titles = title_prop.get("title", [])
        title_text = titles[0]["plain_text"] if titles else "(タイトルなし)"
    else:
        titles = result.get("title", [])
        title_text = titles[0]["plain_text"] if titles else "(タイトルなし)"
        
    print(f"[{obj_type.upper()}] {title_text} (ID: {item_id})")

検索対象の絞り込み(ページのみ / データベースのみ)

filter オプションを指定することで、検索結果をページのみ、またはデータベースのみに限定できます。

# データベースのみを検索対象にする
db_results = notion.search(
    query="プロジェクト",
    filter={
        "value": "database",
        "property": "object"
    }
)

ユーザー情報(Users API)の取得

Notion ワークスペース内のユーザー一覧や、特定のユーザー情報を取得できます。 担当者プロパティ(People)にユーザーを割り当てたい場合などに、ユーザーの id を特定するために利用します。

ワークスペースのユーザー一覧取得

users_response = notion.users.list()

for user in users_response.get("results", []):
    user_id = user["id"]
    name = user.get("name", "名称不明")
    user_type = user.get("type")  # "person" または "bot"
    
    email = ""
    if user_type == "person":
        email = user.get("person", {}).get("email", "")
        
    print(f"名前: {name} | 種別: {user_type} | メール: {email} | ID: {user_id}")

自分自身(Bot)の情報を取得する

インテグレーション自身の名前やワークスペース内での ID を確認するには、users.me() を使用します。

my_bot = notion.users.me()
print(f"Bot 名: {my_bot.get('name')}")
print(f"Bot ユーザーID: {my_bot.get('id')}")

第7章 実践自動化レシピ

Notion API と Python を組み合わせた、実用的で日常の作業を効率化する 3 つの実践自動化レシピを紹介します。

レシピ 1:毎日の「日報ページ」を自動起票する

毎日朝に実行し、その日の日付をタイトルにした日報ページを自動生成するスクリプトです。 親ページ配下に「本日の予定」「振り返り」「メモ」のブロックをあらかじめ用意します。

import os
from datetime import datetime
from dotenv import load_dotenv
from notion_client import Client

load_dotenv()
notion = Client(auth=os.getenv("NOTION_TOKEN"))
parent_page_id = os.getenv("NOTION_DAILY_PAGE_ID")

today_str = datetime.now().strftime("%Y-%m-%d")
page_title = f"{today_str} 日報"

# 本文の初期テンプレート
children_blocks = [
    {
        "object": "block",
        "type": "heading_2",
        "heading_2": {
            "rich_text": [{"type": "text", "text": {"content": "本日のタスク・予定"}}]
        }
    },
    {
        "object": "block",
        "type": "to_do",
        "to_do": {
            "rich_text": [{"type": "text", "text": {"content": "朝会に参加する"}}],
            "checked": False
        }
    },
    {
        "object": "block",
        "type": "heading_2",
        "heading_2": {
            "rich_text": [{"type": "text", "text": {"content": "ふりかえり・メモ"}}]
        }
    },
    {
        "object": "block",
        "type": "paragraph",
        "paragraph": {
            "rich_text": [{"type": "text", "text": {"content": ""}}]
        }
    }
]

new_daily_page = notion.pages.create(
    parent={"page_id": parent_page_id},
    properties={
        "title": [{"text": {"content": page_title}}]
    },
    children=children_blocks
)

print(f"日報ページを作成しました: {page_title} (ID: {new_daily_page['id']})")

レシピ 2:Web サイトの情報を収集してデータベースに自動保存する

Web サイトのニュースや技術記事のタイトルと URL を取得し、Notion の「あとで読む」データベースに自動登録するスクリプトです。

import os
from dotenv import load_dotenv
from notion_client import Client

load_dotenv()
notion = Client(auth=os.getenv("NOTION_TOKEN"))
database_id = os.getenv("NOTION_DATABASE_ID")

# 収集した記事のサンプルデータ(スクレイピング結果など)
articles = [
    {"title": "Notion API 活用入門", "url": "https://example.com/notion-guide", "category": "Tech"},
    {"title": "Python による業務自動化の基本", "url": "https://example.com/python-auto", "category": "Python"}
]

for article in articles:
    notion.pages.create(
        parent={"database_id": database_id},
        properties={
            "記事タイトル": {
                "title": [{"text": {"content": article["title"]}}]
            },
            "URL": {
                "url": article["url"]
            },
            "カテゴリ": {
                "select": {"name": article["category"]}
            },
            "既読": {
                "checkbox": False
            }
        }
    )
    print(f"保存完了: {article['title']}")

レシピ 3:requests ライブラリによる生の REST API 操作

外部ライブラリ notion-client を使わず、標準的な HTTP 通信ライブラリ requests のみを用いて Notion API を呼び出すパターンです。 SDK が利用できない環境や、より軽量なプログラムを作成したい場合に有効です。

import os
import requests
from dotenv import load_dotenv

load_dotenv()
token = os.getenv("NOTION_TOKEN")
database_id = os.getenv("NOTION_DATABASE_ID")

url = f"https://api.notion.com/v1/databases/{database_id}/query"
headers = {
    "Authorization": f"Bearer {token}",
    "Notion-Version": "2022-06-28",
    "Content-Type": "application/json"
}

# POST リクエストでデータベースを検索
response = requests.post(url, headers=headers, json={"page_size": 5})

if response.status_code == 200:
    data = response.json()
    for row in data.get("results", []):
        row_id = row["id"]
        print(f"レコード ID: {row_id}")
else:
    print(f"エラー発生: {response.status_code} - {response.text}")

第8章 エラーハンドリングと運用のベストプラクティス

Notion API を安定して運用するためのエラーハンドリング手法、レートリミット対策(指数バックオフ)、およびセキュリティ面での注意点を解説します。

主要な HTTP ステータスコードとエラー種別

Notion API で問題が発生した場合、標準的な HTTP ステータスコードとともに、JSON 形式のエラー詳細が返されます。

HTTP コードNotion エラーコード主な原因と対策
400validation_errorリクエストの JSON 形式やプロパティの型が不正。型定義を見直す。
401unauthorizedトークンが無効または指定されていない。.env の値を確認する。
404object_not_found指定した ID のページ・DB が存在しない、またはインテグレーションが未接続。
429rate_limitedリクエスト頻度が高すぎる。少し待機してから再試行する。
500 / 502internal_server_errorNotion 側のサーバー障害。時間をおいて再試行する。

SDK によるエラーハンドリングの実装

notion-client を使用する場合、API の失敗は APIResponseError 例外として発生します。 例外をキャッチすることで、プログラムが異常終了するのを防ぎ、適切な対処を行えます。

import os
from dotenv import load_dotenv
from notion_client import Client
from notion_client.errors import APIResponseError

load_dotenv()
notion = Client(auth=os.getenv("NOTION_TOKEN"))

try:
    response = notion.pages.retrieve(page_id="invalid_page_id")
except APIResponseError as error:
    print(f"Notion API エラー検知!")
    print(f"ステータスコード: {error.status}")
    print(f"エラーコード: {error.code}")
    print(f"メッセージ: {error.message}")
except Exception as e:
    print(f"予期しないエラー: {e}")

レートリミット(429)と指数バックオフ

Notion API の制限目安は「1 秒あたり平均 3 リクエスト」です。 大量のデータを一度に更新・取得しようとすると、429 Rate Limited エラーが発生することがあります。

429 エラーが返された場合は、レスポンスヘッダーの Retry-After(秒数)を参照して待機するか、徐々に待機時間を延ばしながら再試行する 指数バックオフ(Exponential Backoff) を実装します。

import time
import requests

def request_with_retry(url, headers, max_retries=5):
    delay = 1.0  # 初期待機時間(秒)
    
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)
        
        # 成功した場合はレスポンスを返す
        if response.status_code == 200:
            return response.json()
            
        # レートリミットの場合は待機して再試行
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", delay))
            print(f"レートリミット発生。{retry_after} 秒待機します(試行 {attempt + 1}/{max_retries})")
            time.sleep(retry_after)
            delay *= 2  # 待機時間を倍増
            continue
            
        # その他のエラーは中断
        response.raise_for_status()
        
    raise RuntimeError("最大再試行回数を超過しました。")

運用のセキュリティ・ベストプラクティス

  1. シークレットキーの分離: トークンはソースコードに直接書かず、環境変数やシークレット管理ツールで保持します。
  2. 接続権限の最小化: インテグレーションには必要なページやデータベースのみを接続し、ワークスペース全体への無制限なアクセスを避けます。
  3. ログ出力時のマスキング: トークンや個人情報(メールアドレス等)がログファイルに出力されないよう配慮します。
  4. 定期的な棚卸し: 使わなくなったインテグレーションや接続権限は、開発者ポータルおよびページの設定から削除します。