Skip to content

How to Integrate with the Notion API: Architecture Guide for B2B SaaS

Notion's block-based API requires recursive traversal, handles just 3 requests/second, and caps payloads at 1,000 blocks. Here's how to architect a production-ready integration.

Roopendra Talekar Roopendra Talekar · · 12 min read
How to Integrate with the Notion API: Architecture Guide for B2B SaaS

If you're building B2B software in 2026, your customers expect their tools to talk to their knowledge base. And increasingly, that knowledge base is Notion. Notion reached 100 million users worldwide in 2024, a 5x growth from 20 million users in 2022. In 2025, over 50% of Fortune 500 companies used the platform. Product requirements documents, customer research, meeting notes, and architecture diagrams all live inside Notion workspaces. When your SaaS platform can't read or write to that workspace, you force users into manual copy-pasting — a friction point that kills enterprise deals.

Here's what you need to know upfront: Notion's API is unlike most REST APIs you've worked with. It doesn't return flat documents. It returns a graph of nested block objects, and reconstructing a full page from that graph requires recursive API calls, pagination at every level, and careful respect for some of the tightest rate limits in the SaaS ecosystem.

Most engineering teams approach the Notion API the same way they approach a standard CRM (like HubSpot) or ticketing API (like Intercom). They write a quick wrapper, deploy it, and move on. Two weeks later, their logs are flooded with 429 Too Many Requests errors—a common breaking point when handling API rate limits at scale—silent pagination failures, and incomplete document renders.

This guide breaks down the architectural realities of the Notion API — the technical limitations of its block-based structure, the math behind its strict rate limits, the nightmare of recursive pagination, and how to architect an integration that actually scales for B2B workloads.

Why Your B2B SaaS Needs a Notion Integration

The demand signal is real. Notion AI has significantly contributed to workplace adoption. In 2023, the ratio of individual to company customers shifted from 90:10 to 50:50, indicating broader enterprise penetration. That shift means Notion is no longer a productivity toy for startups — it's infrastructure for mid-market and enterprise teams.

Teams running RAG pipelines want to ingest Notion pages for contextual Q&A. Documentation platforms want to sync release notes. Compliance tools need to back up intellectual property. And project management products need bidirectional task syncing with Notion databases. When your customers ask for a Notion integration, they expect it to work natively — not as a glorified webhook that loses formatting.

But enterprise adoption also means your integration can't be a weekend hack. It needs to handle large workspaces, respect security boundaries, and survive the API's quirks under load.

Understanding Notion's Architecture: Blocks vs. Standard REST

To understand why integrating with Notion is difficult, you have to understand how Notion stores data.

Standard REST APIs typically return flat JSON objects. If you request a contact from a CRM, you get a single JSON object containing that contact's fields. If you request a ticket from a helpdesk, you get a single object with a description and status.

Notion is fundamentally different. Notion's content is divided into blocks, which are the fundamental units of data. Each block can contain various types of content, from text and images to database entries. A Notion "page" is really a root block with children. Those children can be paragraphs, headings, toggle lists, columns, synced blocks, or embedded databases — and any of those children can have their own children. It's a tree, not a table.

Here's what a single Notion page looks like from the API's perspective:

graph TD
    A["Page (root block)"] --> B["Heading 1 block"]
    A --> C["Paragraph block"]
    A --> D["Toggle block<br>has_children: true"]
    D --> E["Nested paragraph"]
    D --> F["Bulleted list item<br>has_children: true"]
    F --> G["Sub-list item"]
    F --> H["Sub-list item"]
    A --> I["Column list block<br>has_children: true"]
    I --> J["Column 1"]
    I --> K["Column 2"]
    J --> L["Paragraph in col 1"]
    K --> M["Image in col 2"]

When you call the Retrieve block children endpoint, it returns only the first level of children for the specified block. The response will not contain those children, but the has_children property will be true. If your integration needs a complete representation of a page's content, it should search the results for blocks with has_children set to true, and recursively call the retrieve block children endpoint.

Here's a simplified example of what a single paragraph block looks like in the API response:

{
  "object": "block",
  "id": "c02fc1d3-db8b-45c5-a222-27595b15aea7",
  "type": "paragraph",
  "has_children": false,
  "paragraph": {
    "rich_text": [
      {
        "type": "text",
        "text": {
          "content": "This is a ",
          "link": null
        }
      },
      {
        "type": "text",
        "text": {
          "content": "bold",
          "link": null
        },
        "annotations": {
          "bold": true
        }
      }
    ]
  }
}

Notice the has_children boolean. If a block is a toggle list or a column layout, has_children will be true. To get the content inside it, you must make a completely separate API call using that specific block's ID. This means fetching a single Notion page with any nesting at all is never a single API call — it's a full tree traversal. And every level of that tree is independently paginated.

As we documented in our guide on converting Notion to PDF via API, reconstructing a full page requires traversing this tree, fetching children for every nested block, and stitching the resulting graph back together into a linear format. The default approach essentially requires a dedicated microservice.

Because Notion's data model is so granular, API consumers are forced to make a high volume of requests just to read a single document. To protect its infrastructure, Notion enforces aggressive limitations that will shape your entire architecture.

The 3 Requests Per Second Limit

The rate limit for incoming requests per integration is an average of three requests per second. A Notion engineer shared additional context: the rate limit is 2,700 calls every 15 minutes per token.

All plans share the same 3 requests/second average limit (2,700 per 15 minutes). There's no paid tier with higher API access. Overages result in HTTP 429 throttling, not charges.

Three requests per second is extremely low for B2B SaaS workloads. If you're building an AI agent that needs to ingest a customer's entire Notion workspace for Retrieval-Augmented Generation (RAG), you'll hit this limit almost instantly.

Integrations should accommodate variable rate limits by handling HTTP 429 responses and respecting the Retry-After response header value, which is set as an integer number of seconds.

To handle this, your integration architecture must implement a token bucket algorithm or a strict queueing system with exponential backoff. You cannot simply fire off concurrent Promise.all() requests in Node.js. You must serialize your calls or use a controlled concurrency pool that strictly meters outbound HTTP traffic to stay under the limit.

For more on architecting resilient systems, see our guide on best practices for handling API rate limits.

Payload Size and Nesting Limits

Reading data is only half the battle. Writing data back to Notion introduces its own strict constraints.

If your SaaS platform needs to programmatically generate Notion pages — perhaps exporting a weekly analytics report or generating release notes — you're bound by Notion's payload limits:

  • Payloads have a maximum size of 1,000 block elements and 500KB overall.
  • Nested blocks (e.g. nested bullet lists) can only go two levels deep. These limitations can make it difficult to work with large or complex payloads.
  • Any individual children array is capped at 100 elements per request.

If your document requires three levels of nesting (for example, a bulleted list inside a toggle block inside a column), you cannot create it in one go. You must create the top two levels, wait for the response to get the newly generated block IDs, and then make subsequent API calls to append the third level.

Warning

Architectural Reality: Never assume a document export will succeed in a single HTTP request. Your system must be capable of chunking large documents into batches of 1,000 blocks, validating payload sizes, and maintaining a state machine to handle deep nesting across multiple sequential API calls.

Warning

Watch out for file URL expiration. Each time a database or page is queried, a new public URL is generated for all files hosted by Notion. The public URLs are updated hourly and the previous public URLs are only valid for one hour. If your integration caches Notion file URLs, they'll break within sixty minutes.

The Nightmare of Recursive Pagination in Notion

This is where most teams underestimate the engineering effort. The maximum number of results in one paginated response is 100. That limit applies at every level of the block tree.

The Math

Let's look at a concrete scenario. Imagine a customer has a deeply detailed architecture document. The root page has 250 blocks. 50 of those blocks are toggles, and each toggle contains 150 child blocks.

To read this single document, your system must execute:

  1. Fetch root blocks (Items 1–100)
  2. Fetch root blocks (Items 101–200)
  3. Fetch root blocks (Items 201–250)
  4. For each of the 50 toggles, fetch child blocks (Items 1–100)
  5. For each of the 50 toggles, fetch child blocks (Items 101–150)

That's 3 + 50 + 50 = 103 separate API calls just to read one document. At the strict rate limit of 3 requests per second, fetching this single page takes over 34 seconds.

Scale that to a workspace with thousands of pages and you're staring at minutes — or hours — of wall-clock time per sync.

The Implementation

The recursive traversal code itself isn't trivial either. You need to:

  • Build a queue or recursive function that tracks parent-child relationships
  • Handle cursor-based pagination (next_cursor / has_more) at every level
  • Implement retry logic with exponential backoff for 429 responses
  • Reassemble the tree in the correct order after all requests complete
  • Handle edge cases like synced blocks (which reference content from other pages)

Reading large pages may take some time. Notion recommends using asynchronous operations in your architecture, such as a job queue. You will also need to be mindful of rate limits to appropriately slow down making new requests after the limit is met.

If your integration layer doesn't have a declarative pagination system that automatically follows next_cursor tokens across nested resources—or a broader strategy to normalize pagination and error handling—you'll have to write and maintain complex recursive functions. In production, these scripts frequently time out, drop data, or crash your worker processes due to memory leaks when processing massive workspaces.

Webhooks: Better Than Before, But Still Evolving

For a long time, the Notion API had no webhook support at all, forcing teams to poll for changes. That's changed. Webhooks let your integration receive real-time updates from Notion. Whenever a page or database changes, Notion sends a secure HTTP POST request to your webhook endpoint. This allows your application to respond to workspace activity as it happens.

Notion's API version 2025-09-03 introduced integration webhooks alongside the new multi-source database model. Integration webhooks enable integrations to monitor and respond to changes in Notion workspaces in real-time. When changes occur in pages or databases shared with your integration, Notion automatically sends notifications to your webhook endpoint.

But there are still notable limitations. Integration webhooks currently do not support notifications for user changes (including workspace membership changes, email/name updates, and permission modifications) or workspace and teamspace settings changes. And webhook payloads are lightweight event notifications — they tell you what changed, not the full content. You still need to call the API to fetch the updated data, which brings you right back to the rate limit and recursive pagination challenges described above. Processing these real-time events reliably across your customer base requires a robust architecture for handling webhooks at scale.

The UX Challenge: Building a Notion Page Selector

There's an engineering challenge hiding behind the API, and it's a UX problem: letting your customers choose which Notion pages to sync.

Notion workspaces can contain tens of thousands of pages in nested hierarchies. When a customer connects their Notion account to your product via OAuth 2.0, enterprise security teams expect granular access control. They don't want to grant your B2B SaaS platform read/write access to their entire corporate workspace. They want to select specific pages or databases to sync. This is especially critical for customers under SOC 2 or GDPR.

Building this selector means:

  1. Exchanging the OAuth code for an access token.
  2. Querying the Notion Search endpoint to find all accessible pages.
  3. Recursively querying pages to map parent-child relationships.
  4. Rendering a dynamic tree UI in your frontend that handles workspaces with 40,000+ pages without freezing.
  5. Persisting selections and respecting them during incremental syncs.

Notion's own OAuth flow includes a basic page picker, but it's limited — it doesn't show parent-child relationships or support the kind of granular selection enterprise customers expect. If you get this wrong, you either ask for too many permissions (causing security reviews to fail) or you build a clunky UI where users have to manually paste Notion URLs into text boxes.

As we noted in our breakdown of Truto's Notion integration, solving this requires dedicated frontend components tightly coupled to your backend token management system. It is not a weekend project.

How to Integrate with the Notion API Faster Using a Unified API

You have two paths for building a Notion integration.

Path 1: Build it directly. You write the recursive block traversal logic, implement rate limiting with exponential backoff, handle OAuth token refresh, build a page selector UI, and maintain it all when Notion ships breaking changes (like the 2025-09-03 migration to data sources). Budget 4–8 weeks of engineering time for a production-grade integration, plus ongoing maintenance.

Path 2: Use an abstraction layer. A unified API like Truto's Unified Knowledge Base API normalizes Notion's block tree into a standardized schema alongside other wiki platforms like Confluence and Slab.

Normalized Document Schemas

The Unified Knowledge Base API translates Notion's granular block graph into standardized PageContent entities. Whether the underlying data comes from Notion, Confluence, or Slab, your application receives a clean, flat representation of the document. You don't need to write integration-specific parsing logic.

Built-in Rate Limiting and Pagination

The platform automatically handles Notion's strict 3 requests per second limit with built-in exponential backoff and circuit breakers. When you request a deeply nested page, the recursive next_cursor pagination is handled automatically — traversing the tree and delivering the complete dataset to your application without timing out.

OAuth Lifecycle Management

Truto handles OAuth token refresh proactively, refreshing tokens before they expire and alerting you if re-authentication is needed. You don't build or maintain the token management plumbing, which helps avoid the hidden maintenance costs of native integrations.

Drop-in Page Selection UI

To solve the UX challenge of granular permissions, Truto provides RapidForm — a drop-in UI component that allows your users to securely browse and select specific Notion pages, databases, and parent-child hierarchies. This ensures you pass enterprise security reviews by requesting access only to the pages your customer authorizes, skipping months of UI development.

Here's what a basic page content fetch looks like through the unified API:

# Fetch pages from a connected Notion account
curl -X GET "https://api.truto.one/unified/knowledge-base/pages?integrated_account_id=abc123" \
  -H "Authorization: Bearer YOUR_TRUTO_TOKEN"

The response comes back in a normalized schema — the same structure you'd get from Confluence, Slab, or any other supported wiki provider. No recursive traversal, no block-to-markdown conversion, no file URL expiration to worry about.

The honest trade-off: A unified API adds a dependency and a layer of abstraction. If you need pixel-perfect control over every block type, or you're building features that depend on Notion-specific functionality (like database views or relations), you may still need direct API access for some operations. Truto's Proxy API gives you an escape hatch for those cases, handling auth and pagination while letting you work with the raw Notion data model.

Decision Framework

Factor Build Directly Use a Unified API
Time to ship 4–8 weeks Days
Notion-specific features Full access Standardized subset + proxy escape hatch
Multi-provider support One integration at a time Notion + Confluence + Slab from one interface
Maintenance burden Your team owns it Provider handles API changes
Rate limit management Custom implementation Built-in
Page selector UX Build from scratch Drop-in component

If Notion is the only knowledge base your customers use and you have engineering bandwidth to spare, building directly gives you maximum control. If your customers use Notion and Confluence and Google Docs — which is the reality for most B2B SaaS products serving mid-market and enterprise — a unified API collapses the problem from N integrations down to one.

Ship It Without Burning a Quarter on It

Notion's API is powerful, but it's architecturally unusual. The block tree structure, strict rate limits, payload caps, and recursive pagination requirements mean a production-grade integration takes real engineering investment.

The teams that ship Notion integrations fast tend to do one of two things: either they have a dedicated integrations team with deep API experience, or they use an abstraction layer that handles the hard infrastructure problems so they can focus on the user experience.

Whichever path you choose, don't underestimate the recursive pagination problem. It's the thing that turns a "two-week integration" into a quarter-long project. Plan for it, or plan around it.

FAQ

What is the Notion API rate limit?
The Notion API enforces an average rate limit of 3 requests per second per integration (2,700 requests per 15 minutes). This applies to all pricing tiers with no option to pay for higher limits. Rate-limited requests return HTTP 429 with a Retry-After header.
How do you handle nested blocks in the Notion API?
The Notion API only returns first-level children for any block. You must check each block's has_children property and recursively call the retrieve block children endpoint to reconstruct a full page, handling cursor-based pagination at every level.
What are the Notion API payload size limits?
Notion limits request payloads to 1,000 block elements and 500KB total. Any individual children array is capped at 100 elements, and nested blocks can only go two levels deep in a single request.
Does Notion support webhooks?
Yes, as of API version 2025-09-03, Notion supports integration webhooks for real-time page and database change notifications. However, webhook payloads are lightweight event notifications — you still need to call the API to fetch the actual updated content.
How long does it take to build a Notion API integration?
A production-grade Notion integration typically takes 4-8 weeks of engineering time, accounting for recursive block traversal, rate limit handling, OAuth management, and a page selector UI. Using a unified API can reduce this to days.

More from our Blog