Notion API: как подключить базы Notion к сайту или боту
Продвинутое4 минРедакция Innovialabs
Коротко
Создайте внутреннюю интеграцию, подключите её к нужной базе через меню Connections и обращайтесь к API с токеном на сервере: запрос базы отдаёт до 100 записей за раз, а при ответе 429 нужно подождать и повторить.
Коротко: как это работает
Notion API — это REST API, через который ваш сервер читает и меняет страницы и базы данных Notion. Схема всегда одна:
- Создать интеграцию и получить секретный токен.
- Поделиться с интеграцией конкретными страницами или базами — по умолчанию она не видит ничего.
- Отправлять запросы к
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 | Хорошо, с кэшем | Избыточно |
| Заявки от бота в небольшом объёме | Удобно для команды | Тоже подходит |
| Высокая нагрузка, частые записи | Упрётся в лимиты | Нужна |
| Сложные связи и транзакции | Не подходит | Нужна |