Перейти к содержанию
Innovialabs

Notion API: как подключить базы Notion к сайту или боту

Продвинутое4 мин

Коротко

Создайте внутреннюю интеграцию, подключите её к нужной базе через меню Connections и обращайтесь к API с токеном на сервере: запрос базы отдаёт до 100 записей за раз, а при ответе 429 нужно подождать и повторить.

Коротко: как это работает

Notion API — это REST API, через который ваш сервер читает и меняет страницы и базы данных Notion. Схема всегда одна:

  1. Создать интеграцию и получить секретный токен.
  2. Поделиться с интеграцией конкретными страницами или базами — по умолчанию она не видит ничего.
  3. Отправлять запросы к api.notion.com с токеном и заголовком Notion-Version.

Так Notion превращается в лёгкую CMS для блога, базу заявок для Telegram-бота или справочник для внутреннего сервиса.

Шаг 1. Создайте интеграцию

  • Откройте раздел интеграций в настройках Notion и создайте internal integration для своего workspace.
  • Выберите возможности (capabilities): только чтение контента или ещё и вставка/изменение. Давайте минимум, который нужен.
  • Скопируйте Internal Integration Secret и храните его в переменных окружения сервера.

Важно: токен никогда не должен попадать во фронтенд или в код мобильного приложения. Все запросы к Notion идут только с вашего бэкенда.

Шаг 2. Дайте доступ к базе

Интеграция видит только то, чем с ней поделились. Откройте страницу базы, в меню «...» выберите Connections и добавьте свою интеграцию. Доступ наследуется дочерними страницами.

ID базы берётся из её URL — это 32-символьная строка перед ?v=.

Самая частая ошибка новичков — ответ object_not_found при правильном ID. Почти всегда это значит, что базу забыли подключить к интеграции.

Шаг 3. Чтение базы с пагинацией и лимитами

Запрос к базе возвращает максимум 100 записей за раз. Если в ответе has_more: true, повторите запрос с start_cursor из next_cursor.

Лимит запросов — в среднем около трёх в секунду на интеграцию. При превышении API отвечает 429 и заголовком Retry-After — нужно подождать указанное число секунд.

const headers = {
  Authorization: `Bearer ${process.env.NOTION_TOKEN}`,
  "Notion-Version": "2022-06-28",
  "Content-Type": "application/json",
};

async function queryAll(databaseId, body = {}) {
  const results = [];
  let cursor;
  do {
    const res = await fetch(`https://api.notion.com/v1/databases/${databaseId}/query`, {
      method: "POST",
      headers,
      body: JSON.stringify({ ...body, page_size: 100, start_cursor: cursor }),
    });
    if (res.status === 429) {
      const wait = Number(res.headers.get("Retry-After") || 1);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`Notion API error ${res.status}`);
    const data = await res.json();
    results.push(...data.results);
    cursor = data.has_more ? data.next_cursor : undefined;
  } while (cursor);
  return results;
}

В новых версиях API базы разделены на data sources, и эндпоинт запроса отличается. Если вы выбираете свежую Notion-Version, сверьтесь с официальной документацией.

Пример: Notion как CMS для блога

Заведите базу со свойствами Name (title), Slug (text), Status (select), Date (date). Получаем только опубликованные посты:

const pages = await queryAll(process.env.NOTION_POSTS_DB, {
  filter: { property: "Status", select: { equals: "Published" } },
  sorts: [{ property: "Date", direction: "descending" }],
});

const text = (arr) => arr.map((t) => t.plain_text).join("");

const posts = pages.map((p) => ({
  id: p.id,
  title: text(p.properties.Name.title),
  slug: text(p.properties.Slug.rich_text),
  date: p.properties.Date.date?.start,
}));

Если Status — это свойство типа status, а не select, фильтр пишется как status: { equals: "Published" }.

Сам текст статьи — это блоки: GET /v1/blocks/{page_id}/children. Они тоже приходят страницами по 100, а вложенные блоки (has_children: true) нужно запрашивать отдельно.

Запись: заявки из бота в базу

Новая запись — это новая страница с родителем-базой:

await fetch("https://api.notion.com/v1/pages", {
  method: "POST",
  headers,
  body: JSON.stringify({
    parent: { database_id: process.env.NOTION_LEADS_DB },
    properties: {
      Name: { title: [{ text: { content: name } }] },
      Phone: { phone_number: phone },
      Source: { select: { name: "Telegram" } },
    },
  }),
});

Изменить запись можно через PATCH /v1/pages/{page_id} с теми же объектами свойств.

Частые ошибки

  • Запрос к Notion на каждый просмотр страницы. Лимиты быстро закончатся. Кэшируйте результат или генерируйте страницы статически с периодическим обновлением.
  • Привязка к названиям свойств. Переименовали колонку — интеграция сломалась. Для надёжности используйте ID свойств.
  • Ссылки на файлы. URL загруженных в Notion изображений временные. Для сайта копируйте файлы в своё хранилище.
  • Длинный текст в одном объекте. У rich text есть ограничение длины на один текстовый объект — длинный текст разбивайте на части.
  • Нет обработки 429 и 5xx. Повторы с паузой обязательны для любой интеграции.

Когда Notion подходит, а когда нет

ЗадачаNotionПолноценная БД/CMS
Блог, справочник, FAQХорошо, с кэшемИзбыточно
Заявки от бота в небольшом объёмеУдобно для командыТоже подходит
Высокая нагрузка, частые записиУпрётся в лимитыНужна
Сложные связи и транзакцииНе подходитНужна

Частые вопросы

Нет. Токен даёт доступ ко всем подключённым страницам, поэтому запросы делают только с сервера — например, из API-роута или серверной функции.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.