はじめに
はじめてNotion API(Public integration)を使うとき、情報が少なく実装に苦戦した😢
この記事では苦戦した経験をもとに、モバイルアプリにNotion API(Public integration)を実装する手順を丁寧に解説する✨
対象読者
- モバイルアプリにNotion API(Public Integration)を実装したい方
 
この記事で作るもの
✅アプリ利用者がNotionと連携して、APIが使える状態にする。
(最後におまけでAPIの実行も解説する)
他にいい方法があればX(@rakuraku_eng)にメッセージいただけると嬉しいです!
解決したい課題
初心者が一から調べてNotion API(Public integration)を実装するのは大変💦
この記事では、初心者でも手順どおりに真似すればNotion API(Public integration)を実装できるように解説する😊
Notion API(Public integration)って何?
Notion APIには2種類のインテグレーションがある
前提としてNotion APIを使うには、インテグレーション(APIキーのようなもの)が必要。
インテグレーションは2種類あり、Public Integrationはそのうちの1種類。
- Internal Integration:自分用のアプリ開発で使うインテグレーション。
 - Public integration:多数のユーザーに配布するアプリ開発で使うインテグレーション。
 
【補足】Internal Integrationの詳細
✅一言でいうと非公開用のインテグレーション。
アプリ利用者全員がNotionの開発者ページに行って、自分用のインテグレーションを発行する必要がある。
- メリット :実装が簡単
 - デメリット:ユーザーが面倒(インテグレーションを発行するのが手間)
 - 主な用途 :自分でAPIを使用するとき
 
【補足】Public integrationの詳細
✅一言でいうと公開用のインテグレーション。
開発者だけがNotionの開発者ページに行って、インテグレーションを発行すればいい。(皆で1つのインテグレーションを使う)
- メリット :ユーザーが楽(インテグレーションを発行する手間がない)
 - デメリット:実装が難しい
 - 主な用途 :多数のユーザーにAPIを使ってもらうするとき
 - イメージ :利用者はアクセス許可するだけでNotion APIを利用できる✨
※アクセス許可の画面
 
Public integrationが難しく感じる理由
1つ1つの手順はそれほどややこしくないが、使用する技術が多いせいで、知らないものがあると難しく感じてしまう。
今回はこれらを知らない人でも実装できるように向けに丁寧に解説する✨
【補足】主な使用技術
- OAuth2.0
→アクセストークンを取得するためにOAuth2.0を使う。
 - ディープリンク
→アクセストークンを受け取る過程で必要。
(Webページを開く処理が出てくる。そのときモバイルアプリに戻ってくるためにディープリンクが必要。)
 - GitHub Pages(Webページを作成)
→アクセストークンを受け取る過程で必要。
(Notion側の仕様でモバイルアプリでは一時トークンを受け取れない。一時トークンを受け取るためにWebページが必要。)
 - AWS Lambda(APIエンドポイントを作成)
→アクセストークンを受け取る過程で必要。
(セキュリティ的にモバイルアプリから直接アクセストークンを発行するのはNG。間にAWS Lambdaが必要。)
 
Public Integrationを使うためのポイントまとめ
✅Public Integrationを使うには、OAuthの認可が必要。
→アクセストークンを取得する必要がある。
→OAuthの認可ができれば、あとは難しいところはほとんどない。
全体の流れ
✅とにかくアクセストークンを取得すればOK!
アクセストークンを取得するには7ステップの作業が必要。
①②③一時トークンをもらう
やりたいこと
✅アクセストークンを取得するため、まずは一時トークンをもらいたい!
ポイント
- Notion側の仕様制限でNotionから直接モバイルアプリに一時トークンを送れない💦
(Webページにしか一時トークンを送れない)
 - つまり必ずどこかのWebページを経由しないといけない💦
 
結論
- 簡単なWebページを作らないといけない。
(GitHub Pagesなどの無料サービスでOK)
 
④⑤⑥⑦アクセストークンをもらう
やりたいこと
✅アクセストークンをもらいたい!
ポイント
- 一時トークン(③で取得したもの)をNotionのサーバーに送れば、アクセストークンが取得できる。
 - しかしモバイルアプリから直接Notionのサーバーに送るのはセキュリティ的にNG💦
(アクセストークンの発行に必要な機密情報をモバイルアプリに持たせるのを避けたい)
 
結論
- AWS Lambdaなどのサービスを介してアクセストークンを発行する。
(無料枠でOK)
 
準備
事前に準備しておくもの
- スマホアプリのプロジェクト
今回はReact Native(Expo)を使う
 - Notionアカウント
 - GitHubアカウント
 - AWSアカウント
 
これから準備するもの
- Notion API
後でAPIを発行する。
 
②③一時トークンをリダイレクトで受け取る
やりたいこと
必要な作業
- Notionが発行してくれた「一時トークン」を受け取り、そのままモバイルアプリに渡すだけの簡単なWebページを作る。
(本当はWebページなしで、直接Notionからモバイルアプリにリダイレクトしたいが、Notionの仕様で必ずWebページを介さないといけない💦)
 - Webページからモバイルアプリを開くためにディープリンクを設定する。
 
【手順1】ディープリンクを準備
✅「Webページ」から「スマホアプリ」にリダイレクトできるようにディープリンクを準備しておく必要がある。
→「myapp」という名前のディープリンク(カスタムURLスキーム)を設定すればOK。
【手順1-1】ディープリンクを設定する
✅Expoではapp.jsonを編集するだけでカスタムURLスキームが設定できる。
app.json
{
  "expo": {
    "scheme": "myapp",  // ✅任意のURLスキーム
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "data": [{
            "scheme": "myapp"  // ✅任意のURLスキーム(android用)
          }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}【手順2】リダイレクト元のHTMLを作成
✅一時トークンを受け取って、リダイレクトするだけのHTMLを作成する。
一時トークンの取得の成功/失敗によって、処理を振り分ける。
一時トークンの取得に成功した場合
一時トークンの取得に失敗した場合
【手順2-1】リダイレクト用のHTML
✅画面はなし。ページを開いたらすぐリダイレクト処理をする。
notion-web/docs/redirects/index.html
(アプリとは別の場所にnotion-webフォルダを新規作成する)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Redirect</title>
  </head>
  <body>
    <script>
      var urlParams = new URLSearchParams(window.location.search);
      var code = urlParams.get("code");
      if (code == null){
        window.location.replace(
	        // ✅ここは適宜書き換える
          "https://自分のGitHubのID.github.io/notion-web/redirects/fallback.html"
        );
      } else {
        window.location.replace(
          "myapp://oauth/callback?code=" + code
        );
      }
    </script>
  </body>
</html>【手順3】成功時のリダイレクト先を作成
✅一時トークンの取得に成功したときのリダイレクト先(モバイルアプリの画面)を作成する。
→【手順2】で指定したリダイレクト先「myapp://oauth/callback」に対応する画面を作成する。
【補足】なぜmyapp://〇〇で自分のアプリが開くの?
✅【手順1】でディープリンクを設定したから。
もしこの設定をしていないと「myapp://」で自分のモバイルアプリが開かない。
【手順3-1】成功時の画面
✅一時トークンの取得に成功したときに表示する画面。
app/oauth/callback.tsx
(一旦画面だけ作る。処理は④⑦の解説で追記する。)
import { Text, View } from "react-native"
import { useEffect } from "react"
import { useLocalSearchParams, router } from "expo-router"
export default function Page() {
  // -----------------------------------------------------------------
  // 1. Notion API(Public Integration)の一時トークンを受け取る
  // -----------------------------------------------------------------
  // クエリパラメータから一時トークンを取得
  const { code } = useLocalSearchParams<{ code: string }>()
  // -----------------------------------------------------------------
  // 2. Notion API(Public Integration)のアクセストークンを発行する
  // -----------------------------------------------------------------
	// ④、⑦で追記する。
  }, [])
	// 画面は適当でOK(CSSは省略)
  return (
    <View>
      <Text>Notionの連携が完了しました</Text>
    </View>
  )
}【手順3-2】ルーティング
✅作成した画面とパス「/oauth/callback」と紐づける。
(Expo Routerでは「app/oauth/callback.tsx」というファイルを作ると自動で「/oauth/callback」にルーティングされる)
【手順4】失敗時のリダイレクト先を作成
【手順5】GitHub PagesでWebページを公開する
✅作った2つのHTMLをGitHub Pagesで公開する。
(とにかくWeb上に公開できればいい。GitHub Pages以外でもOK。)
【手順5-1】Webページ公開
公式ドキュメントのとおりの手順でOK。
(notion-webフォルダをリポジトリにして、GitHub Pagesで公開する。)
①アプリからURLを開く
やりたいこと
必要な作業
- 「Notionのページ選択画面」のURLを準備する。
(Notion APIを発行すればURLも発行される)
 - モバイルアプリに「Notionのページ選択画面」のリンクを設置する。
 
【手順1】Notion APIを発行する
✅「Notionのページ選択画面」のURLはNotion APIを発行すると取得できる。
APIは2ステップで発行できる!
【手順1-1】一旦Internal integrationを発行する
最終的にはPublic integrationを発行したいが、いきなりは発行できない。
(一旦Internal integrationを発行する必要がある)
- Notionの開発者ページに移動し、新しいインテグレーションを作成する。
 - 分かりやすい名前(アプリ名など)を付けて送信ボタンをクリックする。
 
【手順1-2】Public integrationに変更する
- 「ディストリビューション」タブに移動して、「パブリックインテグレーションに設定しますか?」をONにする。
 - 下にある「組織情報」を入力する。
※ダミーの情報でも登録できる
 - 下にある「リダイレクトURI」を入力する。
※②③の手順で作ったWebページ(GitHub Pages)のURI「https://自分のGitHubのID.github.io/notion-web/redirects/index.html」を入力する。
 - 機密情報3点をメモしておく。
 
【手順2】モバイルアプリの画面にリンクを設置する
✅モバイルアプリから「Notionのページ選択画面」を開くためリンクを設置する。
【手順2-1】URLを確認する
【手順2-2】リンクを設置する
✅適当な画面にリンクを設置する。
app/index.tsx
(CSSは省略)
import { StyleSheet, Text, View } from "react-native"
import { A } from "@expo/html-elements"
export default function Page() {
  return (
    <View>
      <A href="https://api.notion.com/v1/oauth/authorize?client_id=〇〇&response_type=code&owner=user&redirect_uri=〇〇">
        Notionと連携
      </A>
    </View>
  )
}⑤⑥アクセストークンを取得するAPIを作成
やりたいこと
必要な作業
- AWS Lambdaで「アクセストークン」を取得するAPIを作る。
 
【補足】AWS Lambdaを使わずモバイルアプリでアクセストークンを取得してはだめ?
✅可能だが、セキュリティの観点で非推奨。
- アクセストークンの取得にはシークレット情報が必要。
 - しかしシークレット情報をモバイルアプリに持たせるのはセキュリティ的に非推奨。
 - シークレット情報をモバイルアプリに持たせないためにAWS Lambdaを使っている。
 
【手順1】AWS Lambdaで関数を作成
✅アクセストークンを取得する関数(API)をAWS Lambdaで作成する。
【手順1-1】AWS Lambdaを開く
AWS Lambda
(AWSのアカウントが無い場合は新規作成する)
(クレジットカードの登録が必要だが今回の用途であれば無料枠で収まる想定)
【手順1-2】関数を新規作成する
【手順1-3】コードを貼り付けてデプロイ
index.mjs(このままコピペでOK!)
/* global fetch */
export const handler = async (event) => {
  let response;
  // 環境変数をチェック
  check_env();
  
  // 環境変数を取得
  const clientId     = process.env.OAUTH_CLIENT_ID;     // NotionのOAuthクライアントID
  const clientSecret = process.env.OAUTH_CLIENT_SECRET; // NotionのOAuthクライアントシークレット
  const redirectUri  = process.env.REDIRECT_URI;        // Notionで設定したリダイレクトURI
  // POST送信されたリクエストボディから
  // 「アクセストークンを受け取るための一時トークン」を受け取る
  const body = JSON.parse(event.body);
  const code = body.code;
  
  // 認可用の文字列を生成
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
  
  // Notionからアクセストークンを取得
  let data = 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: code,
      redirect_uri: redirectUri.toString(),
    }),
  });
  
  // アクセストークンをJSON形式に変換
  data = await data.json();
  
  // エラーがある場合
  if (data.error) {
    response = {
      statusCode: 400,
      body: data,
    };
  }
  // 正常な場合
  else{
    response = {
      statusCode: 200,
      body: data,
    };
  }
  
  return response;
};
/*
 * 環境変数が設定済みかチェック
 * 1つでも未設定なら例外をスローする
 */
function check_env(){
  if (!process.env.OAUTH_CLIENT_ID) {
    throw "OAUTH_CLIENT_ID is required.";
  }
  if (!process.env.OAUTH_CLIENT_SECRET) {
    throw "OAUTH_CLIENT_SECRET is required.";
  }
  if (!process.env.REDIRECT_URI) {
    throw "REDIRECT_URI is required.";
  } 
}【手順2】設定の変更(環境変数)
✅3つの機密情報OAUTH_CLIENT_ID、OAUTH_CLIENT_SECRET、REDIRECT_URIを設定する。
【補足】セキュリティを強化する場合はSecret Managerを使う
今回は機密情報を「環境変数」に設定したが、「Secret Mangaer」に設定する方法もある。
セキュリティを気にする場合は「Secret Mangaer」を使うとよさそう💡
【手順2-1】環境変数を設定
【手順3】設定の変更(タイムアウト)
④⑦アクセストークンを取得する
やりたいこと
必要な作業
- アクセストークンを取得する処理を作る。
 - アクセストークンを保存する処理を作る。
 
【手順1】アクセストークン取得、保存
✅③で作った画面に、アクセストークンを取得、保存する処理を追加する。
(画面の見た目は変更なし。処理だけ追記する。)
【手順1-1】パッケージをインストール
✅パッケージ「expo-secure-store」をインストールする。
npx expo install expo-secure-store【補足】expo-secure-storeとは
expo-secure-storeはキーストアを使うためのパッケージ。
アクセストークンはシークレットな情報なので、キーストアなどの安全な場所に保存しておくといい😊
今回のようにExpoを使っている場合は、「Expo SecureStore」を使えば簡単にキーストアにアクセストークンを保存できる✨
await SecureStore.setItemAsync("notion-token", data.access_token);(Expo以外の開発環境の場合は各自でキーストアの使い方を調べてください)
【手順1-2】アクセストークンを取得、保存する処理を追記
✅③で作った画面に処理を追記する。
app/oauth/callback.tsx
import { Text, View } from "react-native"
import { useEffect } from "react"
import { useLocalSearchParams, router } from "expo-router"
// npx expo install expo-secure-storeでインストールしておく
import * as SecureStore from "expo-secure-store"
/**
 * アクセストークンを発行するページ
 *
 * 1. Notion API(Public Integration)の一時トークンを受け取る
 * 2. Notion API(Public Integration)のアクセストークンを発行する
 */
export default function Page() {
  // -----------------------------------------------------------------
  // 1. Notion API(Public Integration)の一時トークンを受け取る
  // -----------------------------------------------------------------
  // クエリパラメータから一時トークンを取得
  const { code } = useLocalSearchParams<{ code: string }>()
  // -----------------------------------------------------------------
  // 2. Notion API(Public Integration)のアクセストークンを発行する
  // -----------------------------------------------------------------
  // ✅追記
  useEffect(() => {
    // 一時トークンが取得できない場合はエラーページ(別途作成しておく)にリダイレクト
    if (typeof code === "undefined") {
      router.replace("/oauth/error")
    }
    
    // AWS Lambdaを介してNotionのアクセストークンを取得
    fetch("【AWS Lamgdaの関数URL】", {
      method: "POST",
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        code: code,
      }),
    }).then(async res => {
	    // レスポンスが返ってきたとき
      res.json().then(async data => {
        if (data.error) {
          throw new Error(data.error)
        }
        // アクセストークンを保存
        await SecureStore.setItemAsync("notion-token", data.access_token);
      }
      ).catch(error => {
        throw new Error(error)
      })
    });
  }, [])
	// 画面は適当でOK(CSSは省略)
  return (
    <View>
      <Text>Notionの連携が完了しました</Text>
    </View>
  )
}アプリ利用者の端末でNotion APIが使えるようになった!
【おまけ】Notion APIを実行する
API実行の流れ
やりたいこと
✅取得したアクセストークンを使って、自由にNotion APIが使いたい!
※今回はNotion APIを使ってこのNotionデータベースからページ一覧(今回は1ページだけ)を取得してみる。
動作イメージ
ポイント
- APIを使うにはアクセストークンが必要。
 - 保存しておいたアクセストークンを取り出せば自由にAPIが使える。
 
結論
- アクセストークンさえ手に入れば簡単にNotion APIが使える。
 
必要な作業
【手順1】画面を作成
✅Notionデータベースを選択し、ページ一覧を表示する画面を作る。
【手順1-1】セレクトボックスのパッケージをインストール
✅パッケージ「@react-native-picker/picker」をインストールする。
npm install @react-native-picker/picker --save【手順1-2】画面を作成
✅一旦APIを使った処理はなしで画面だけ作る
任意の画面.tsx(コピペでOK)
import { useEffect, useState } from "react";
import { Button, Text, View } from "react-native";
import * as SecureStore from "expo-secure-store";
import { Picker } from "@react-native-picker/picker";
import { PickerItemProps } from "@react-native-picker/picker/typings";
/**
 * Notionデータベースを1つ選択して、ページ一覧を取得する画面
 */
export function SelectScreen() {
  const [notionDatabases, setNotionDatabases] = useState<PickerItemProps[]>([]); // Notionデータベースのリスト(アクセス許可済み)
  const [selectedNotionDatabaseID, setSelectedNotionDatabaseID] = useState(""); // 選択したNotionデータベースのID
  const [notion, setNotion] = useState<any>(null); // Notion APIのクライアント
  const [pageList, setPageList] = useState<any[]>([]); // ページのリスト
  /**
   * 初期化
   */
  useEffect(() => {
		// 【手順2-2】で作る
  }, []);
  /**
   * ボタンが押されたときの処理
   */
  function ok() {
		// 【手順2-3】で作る
  }
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>データベースを選択</Text>
      <View
        style={{
          flexDirection: "row",
          alignItems: "center",
          marginTop: 8,
          marginBottom: 32,
          columnGap: 32,
        }}
      >
        <Picker
          style={{ width: 300, backgroundColor: "#f0f0f0" }}
          selectedValue={selectedNotionDatabaseID}
          onValueChange={(itemValue, itemIndex) =>
            setSelectedNotionDatabaseID(itemValue)
          }
        >
          {notionDatabases.map((database) => (
            <Picker.Item
              key={database.value}
              label={database.label}
              value={database.value}
            />
          ))}
        </Picker>
      </View>
      <View
        style={{
          flexDirection: "row",
          alignItems: "center",
          justifyContent: "center",
          columnGap: 32,
        }}
      >
        <Button title="OK" onPress={ok} />
      </View>
      {pageList.map((page, index) => (
        <View key={index}>
          <Text>Title: {page.title}</Text>
          <Text>Tags: {page.tags}</Text>
        </View>
      ))}
    </View>
  );
}【手順2】APIを使う
【手順2-1】Notion APIのパッケージをインストール
✅パッケージ「@notionhq/client」をインストールする。
npm install @notionhq/client(公式ではJavaScriptのみSDKが用意されている。他の言語はコミュニティがSDKを作っている場合があるのでSDKがあるか調べてみるといい!)
【手順2-2】データベースの一覧を取得
✅Notion APInotion.search(…)を使ってデータベースの一覧を取得する。
先ほどのコードのuseEffect()に追記。(コピペでOK)
/**
 * Notionデータベースを1つ選択して、ページ一覧を取得する画面
 */
export function SelectScreen() {
	// 省略
	
  /**
   * 初期化
   */
  useEffect(() => {
	  // ✅追記
    async function init() {
      // アクセストークンを取得
      const access_token = await SecureStore.getItemAsync("notion-token");
      if (!access_token) {
        console.log("アクセストークンが取得できませんでした。");
        return;
      }
      // Notion APIのクライアントを初期化
      const { Client } = require("@notionhq/client");
      setNotion(new Client({ auth: access_token }));
      // Notion APIを利用して、アクセス許可があるすべてのデータベースを取得
      // NOTE:子ページのデータベースも再帰的に含まれる
      const query = ""; // 検索クエリ(データベースのタイトル文字列と比較する)
      const params = {
        query,
        sort: {
          direction: "ascending",
          timestamp: "last_edited_time",
        },
        filter: {
          property: "object",
          value: "database",
        },
        page_size: 100,
      };
      const response = await notion.search(params);
      const databases = response.results; // 検索結果(データベースの配列)
      console.log(databases);
      // データベースを1つずつ処理
      databases.forEach((database: any) => {
        // データベースのタイトルとIDをセット
        setNotionDatabases((prevDatabases) => [
          ...prevDatabases,
          {
            label: database.title[0].plain_text,
            value: database.id,
          },
        ]);
      });
      // 選択状態の初期値を設定
      setSelectedNotionDatabaseID(databases[0] ? databases[0].id : "");
    }
    init();
  }, []);
  return (
		// 省略
  );
}これでセレクトボックスにアクセス許可済みのデータベースをすべて表示できる。
【手順2-3】データベースにあるページ一覧を取得
✅Notion APInotion.databases.query(…)を使ってデータベースにあるページの一覧を取得する。
先ほどのコードのok()に追記。(コピペでOK)
/**
 * Notionデータベースを1つ選択して、ページ一覧を取得する画面
 */
export function SelectScreen() {
	// 省略
  /**
   * ボタンが押されたときの処理
   */
  function ok() {
	  // ✅追記
    async function getPages() {
      // データベースにある全ページを取得(API実行)
      const response_pages = await notion.databases.query({
        database_id: selectedNotionDatabaseID,
      });
      const pages = response_pages.results;
      // ページのタイトルとタグをセット
      setPageList(
        pages.map((page: any) => ({
          title: page.properties.名前.title[0].plain_text,
          tags: page.properties.タグ.multi_select.map((tag: any) => tag.name),
        })),
      );
    }
    getPages();
  }
  return (
		// 省略
  );
}
これでOKボタンをタップすると、選択したデータベースにあるページをすべて表示できる。
(同じ要領でページの作成などもできる)
よくある疑問
まとめ
工程は多いが、1つずつ見ていくとそれほどややこしいことはしていない✨










































