How to Build a Claude-Powered Notion Integration
A Claude-powered Notion integration is a script or service that reads content from Notion pages and databases through Notion's API, sends that content to Claude for processing, and writes the result back into Notion. Common use cases include summarizing long meeting notes, auto-tagging database rows, turning raw notes into structured tasks, and generating first drafts of documents directly inside a workspace.
Building one requires three pieces: a Notion integration with API access to the pages you want to automate, a way to call Claude programmatically, and a small amount of glue code that moves data between the two. This guide walks through each piece, including the parts most tutorials skip — authentication, rate limits, and how to keep the integration running reliably once it's past the demo stage.
Step 1: Set Up Notion API Access
Create an internal integration from Notion's "My Integrations" page and copy the generated secret. Then share the specific pages or databases you want the integration to touch — Notion integrations have zero access by default, so this step is easy to forget and is the most common cause of "unauthorized" errors later.
Notion's REST API gives you three core endpoints you'll use repeatedly:
GET /v1/pages/{page_id}— page propertiesGET /v1/blocks/{block_id}/children— the actual content blocksPATCH /v1/blocks/{block_id}orPOST /v1/pages— writing content back
Notion content is block-based, not plain text, so you'll need to flatten rich text blocks into a string before sending anything to Claude, and convert Claude's output back into block objects when writing results.
Step 2: Get Claude API Access
You need a way to call Claude's Messages API from your code. If you already use Claude.ai and don't want to set up separate API billing, SubToAPI turns your existing Claude access into a standard HTTPS API with an application key (sub_live_...), so you can call it from a serverless function or backend script without managing a second account. Sign up at /signup and grab a key from the dashboard — plans start at €9/month for solo use, with team seats on the Team (€19/seat) and Scale (€49/seat) plans if multiple people will trigger the integration.
A basic request looks like this:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 500,
"messages": [
{"role": "user", "content": "Summarize this Notion page in 3 bullet points: ..."}
]
}'
See /docs/quickstart and /docs/messages for the full request and response schema.
Step 3: Pull Content from Notion
Fetch the block children for a page and concatenate the text so Claude has something coherent to read:
async function getPageText(pageId, notionToken) {
const res = await fetch(
`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`,
{
headers: {
Authorization: `Bearer ${notionToken}`,
"Notion-Version": "2022-06-28",
},
}
);
const data = await res.json();
return data.results
.flatMap((block) => block[block.type]?.rich_text || [])
.map((t) => t.plain_text)
.join(" ");
}
This handles paragraphs, headings, and bullet lists. Tables, callouts, and nested toggles need extra handling — if your workspace uses those heavily, build a small recursive walker instead of a flat map.
Step 4: Send Content to Claude
Once you have the page text, build a prompt that tells Claude exactly what output format to return — this matters more for Notion integrations than most, because you'll be parsing the response programmatically.
async function summarizePage(pageText) {
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 400,
messages: [
{
role: "user",
content: `Summarize the following notes as 3-5 bullet points, no preamble:\n\n${pageText}`,
},
],
}),
});
const data = await res.json();
return data.content[0].text;
}
For tagging or classification tasks, structured tool use is more reliable than parsing free text — define a tool schema with the exact fields you want (tags, priority, due date) and let Claude return structured JSON instead of prose you have to regex.
Step 5: Write the Result Back to Notion
Convert Claude's output into Notion block objects and append them to the page:
async function appendSummary(pageId, summaryText, notionToken) {
const blocks = summaryText.split("\n").filter(Boolean).map((line) => ({
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [{ type: "text", text: { content: line.replace(/^[-•]\s*/, "") } }],
},
}));
await fetch(`https://api.notion.com/v1/blocks/${pageId}/children`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${notionToken}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
body: JSON.stringify({ children: blocks }),
});
}
Triggering the Integration
Three practical options, roughly in order of setup effort:
- Manual trigger — a CLI script you run on demand against a specific page ID. Good for prototyping.
- Scheduled job — a cron job that polls a database for pages with a "needs summary" checkbox and processes them in batch.
- Webhook-driven — pair with an automation tool that watches for database updates and calls your function immediately. This is the closest to real-time but adds another moving part to debug.
For long documents or anything where you want to show progress in a UI while the model works, use streaming instead of waiting for the full response — useful if you're building a sidebar or Slack-style preview rather than a silent batch job.
Handling Scale and Failures
Once the integration runs automatically, two things will eventually go wrong: Notion pages that exceed what fits comfortably in one prompt, and rate limits on either side. Chunk very long pages before sending them to Claude, and add retry logic with backoff around both the Notion and Claude calls — a single flaky request shouldn't corrupt a page or skip a database row. Check /pricing if you're scaling this across a team workspace, since seat-based plans make more sense once more than one person is triggering the integration regularly.
questions
Do I need a separate Claude API account to build this? No — tools like SubToAPI let you call Claude over HTTPS using your existing Claude access, so you can skip setting up separate API billing just for a Notion script.
Can Claude read Notion databases, not just pages? Yes. Query a database with Notion's /v1/databases/{id}/query endpoint to get rows as pages, then fetch each page's blocks the same way you would for a single document.
What's the best way to get structured output instead of plain text? Use tool/function calling instead of parsing free text — define a schema for tags, summaries, or statuses and Claude returns JSON you can map directly onto Notion properties. See /docs/tools for details.