Skip to content

Notion to Webflow Migration: API Limits & Data Mapping

A technical blueprint for migrating from Notion to Webflow. Learn how to map block-based architecture to Webflow's CMS, bypass API limits, and handle images.

Rishabh Rishabh · · 16 min read
Notion to Webflow Migration: API Limits & Data Mapping
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Notion to Webflow Migration: API Limits & Data Mapping

Migrating data from Notion to Webflow is not a standard export-import exercise. You are moving from an infinitely nestable, block-based JSON tree into a rigid, structured Content Management System (CMS) that expects flat text, defined fields, and specific HTML formatting.

If you attempt a basic CSV export from Notion and import it into Webflow, you will lose rich text formatting, break your relational databases, and end up with broken image links. To execute this migration correctly—with zero data loss—you must map Notion's object model to Webflow's CMS architecture, handle temporary image URLs, and respect the API rate limits of both platforms.

This guide covers the exact technical blueprint for migrating from Notion to Webflow, including block-to-HTML conversion, image asset hosting, multi-reference field mapping, a complete property-type translation table, and realistic performance estimates at stated API limits.


The Architectural Mismatch

Before writing a script or configuring a middleware tool, you must understand how these two platforms store data.

Notion treats everything as a block. A page is a block. A paragraph is a block. A database row is a page, which is also a block. When you query the Notion API, you receive an array of JSON objects, each representing a specific block type with its own nested properties.

Webflow treats content as Items within a Collection. A Collection is a flat database schema. An Item is a row in that database. While Webflow supports a Rich Text field, it expects a single, continuous string of validated HTML—not a nested JSON array.

To migrate successfully, your pipeline must:

  1. Extract the block array from Notion.
  2. Parse and convert those blocks into valid HTML.
  3. Download and re-host all image assets.
  4. Push the formatted data into Webflow's specific field schema.

Notion Property Types → Webflow Field Types: Complete Mapping Table

This mapping table is the highest-stakes artifact in any Notion-to-Webflow migration. Webflow's schema is more rigid; several Notion property types have no direct equivalent and require a deliberate degradation strategy before you write a single line of migration code.

Notion Property Type Webflow Field Type Notes / Degradation Strategy
Title Name (required) Direct map. Webflow requires a Name field on every Collection.
Rich Text Rich Text Requires Notion JSON → Markdown → HTML conversion pipeline.
Number Number Direct map.
Select Option (Single) Direct map. Options must pre-exist in Webflow schema or be created via API before item creation.
Multi-Select Option (Multiple) Direct map with same pre-creation requirement.
Date Date/Time Direct map. Convert ISO 8601 string from Notion to Webflow's accepted format.
Checkbox Switch (Boolean) Direct map.
URL Link Direct map.
Email Email Direct map.
Phone Plain Text Webflow has no native Phone field type. Serialize to Plain Text.
Files & Media Image or File Must download from Notion's temporary S3 URL and re-upload to Webflow Assets API.
Relation Reference / Multi-Reference Requires two-pass migration. Target collection items must exist before reference can be set.
Rollup Plain Text or Number No native equivalent. Compute the rollup value in your script and store the result as a static field.
Formula Plain Text or Number No native equivalent. Evaluate the formula result from the Notion API response and store the static output.
Created Time Date/Time Map to a Date field. Note: Webflow's createdOn field is system-managed and cannot be overwritten via the CMS API. Store separately.
Last Edited Time Date/Time Same constraint as Created Time.
Created By Plain Text No equivalent. Serialize to "First Last" string or discard.
Last Edited By Plain Text No equivalent. Serialize or discard.
People Plain Text No equivalent. Serialize as comma-separated names or emails.
Status Option (Single) Map status groups to single-select options. Not all status values may translate meaningfully.
Unique ID Plain Text Store as a plain text field for cross-reference tracking. Useful as a permanent key in your ID mapping store.
Verification Plain Text No equivalent. Serialize state ("verified" / "unverified") as plain text or discard.
Button Discard UI element only. Not data. Cannot and should not migrate.

Key takeaway: Formula, Rollup, Created By, Last Edited By, and People properties have no Webflow equivalent. You must decide whether to compute them into static values or drop them before your migration script runs. Attempting to push these as-is will cause 400 validation errors.


The Image Expiration Trap

The most common failure point in a Notion to Webflow migration is image handling.

When you query a Notion page via the API, image blocks return AWS S3 signed URLs. These URLs expire in exactly one hour. If you map these URLs directly into a Webflow Rich Text or Image field, the images will render correctly on your published site for up to 60 minutes—then every image returns a 403 Forbidden error.

To solve this, your migration pipeline must intercept every image block:

  1. Read the temporary Notion AWS URL.
  2. Download the image file into a buffer.
  3. Upload the buffer to Webflow via the Webflow Assets API (POST /sites/{site_id}/assets).
  4. Retrieve the new, permanent Webflow CDN URL from the API response.
  5. Replace the original S3 URL with the Webflow CDN URL in your HTML payload before writing to the CMS.

Each image requires two API calls (one Notion read, one Webflow write) plus a file transfer. For a migration with 2,000 images, this step alone accounts for 2,000 Webflow API calls at 60 requests/minute—approximately 33 minutes of asset upload time at the rate limit ceiling, before any CMS item writes occur.


Translating Notion Blocks to Webflow Rich Text

Webflow's Rich Text field accepts a defined subset of HTML. It does not return 400 errors for every unsupported tag—in many cases it silently strips disallowed elements—but structurally malformed HTML, unclosed tags, or invalid nesting will cause 400 Bad Request rejections for the entire item payload.

Tags commonly stripped by Webflow's Rich Text sanitizer include: <iframe>, <script>, <style>, <form>, <input>, <details>, <summary>. Tags that render if correctly formed: <p>, <h1><h6>, <strong>, <em>, <a>, <ul>, <ol>, <li>, <blockquote>, <pre>, <code>, <img>, <figure>, <figcaption>. <table> support depends on your Webflow site settings and plan; test explicitly before assuming it passes.

Handling Unsupported Notion Block Types

Notion supports block types that have no Webflow Rich Text equivalent. The table below defines the recommended degradation strategy for each:

Notion Block Type Webflow Rich Text Equivalent Recommended Strategy
Paragraph <p> Direct map.
Heading 1/2/3 <h1> / <h2> / <h3> Direct map.
Bulleted List Item <ul><li> Direct map.
Numbered List Item <ol><li> Direct map.
Quote <blockquote> Direct map.
Code <pre><code> Direct map. Include language as a class attribute if needed.
Image <figure><img> Download, re-upload to Webflow Assets, inject permanent URL.
Divider <hr> Direct map.
Toggle None Flatten: render the toggle heading as <p><strong> and the child content as nested <p> tags.
Callout None Convert to <blockquote> with the icon stripped, or a <div> with a custom CSS class if your Webflow site supports it.
Synced Block Depends on children Dereference: fetch the source block and treat its children as standard blocks.
Columns None Flatten: render each column's children sequentially as standard block flow. Column layout is lost.
Table <table> (conditionally) Convert to HTML table. Test against your Webflow site before assuming acceptance.
Bookmark <a> Extract URL and title. Render as a hyperlink. Preview metadata (OG image, description) is lost.
Embed <a> or stripped Strip the embed. Link to the URL if available. Webflow does not accept arbitrary <iframe> in Rich Text.
Nested Database (inline) None Cannot migrate inline. Migrate the database separately and link to it.
Child Page <a> Render as a hyperlink to the migrated Webflow page if the target has been migrated.
Equation Plain Text or Image Render LaTeX as a code block or pre-render to an image. No native support.

The Markdown Intermediary Pattern

The most reliable conversion architecture is a two-step process: Notion JSON → Markdown → HTML.

Using notion-to-md (Node.js) to convert the raw block array to Markdown, then marked or markdown-it to generate the HTML string, gives you a predictable intermediate representation and makes the sanitization step easier to reason about.

const { Client } = require('@notionhq/client');
const { NotionToMarkdown } = require('notion-to-md');
const { marked } = require('marked');
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
 
const notion = new Client({ auth: process.env.NOTION_TOKEN });
const n2m = new NotionToMarkdown({ notionClient: notion });
 
// Tags Webflow's Rich Text field will reject or strip
const WEBFLOW_DISALLOWED_TAGS = ['script', 'style', 'iframe', 'form', 'input', 'details', 'summary'];
 
function sanitizeForWebflow(html) {
  const window = new JSDOM('').window;
  const DOMPurify = createDOMPurify(window);
  return DOMPurify.sanitize(html, {
    FORBID_TAGS: WEBFLOW_DISALLOWED_TAGS,
    FORBID_ATTR: ['onerror', 'onload', 'onclick'], // strip event handlers
  });
}
 
async function getWebflowRichText(pageId) {
  // 1. Fetch blocks and convert to Markdown
  const mdBlocks = await n2m.pageToMarkdown(pageId);
  const mdString = n2m.toMarkdownString(mdBlocks);
 
  // 2. Convert Markdown to HTML
  const rawHtml = marked.parse(mdString.parent);
 
  // 3. Sanitize for Webflow
  return sanitizeForWebflow(rawHtml);
}

This implementation uses dompurify with jsdom for server-side DOM sanitization—a more robust approach than regex-based tag stripping, which breaks on nested or malformed markup.


Mapping Relations and Multi-Reference Fields

If your Notion workspace uses Relation properties (e.g., a "Blog Posts" database related to an "Authors" database), you must map these to Webflow Reference or Multi-Reference fields.

This requires a two-pass migration architecture. Webflow Reference fields require the specific Webflow Item ID (a 24-character hex string) of the target item. That ID does not exist until the target item has been created in Webflow.

Pass 1: Migrate Parent Entities

  1. Query the Notion "Authors" database.
  2. Push all Authors to the Webflow "Authors" Collection.
  3. For each successfully created Author, store the mapping of notion_page_id → webflow_item_id in a local key-value store (a JSON file is sufficient for migrations under ~5,000 items; use Redis or SQLite for larger sets).

Pass 2: Migrate Child Entities

  1. Query the Notion "Blog Posts" database.
  2. For each post, read the Notion Relation property to get the Author's notion_page_id.
  3. Look up the corresponding webflow_item_id from your mapping store.
  4. Construct the Webflow API payload, passing the webflow_item_id array into the Reference field.
  5. Push the post to Webflow.

If a target item was not successfully migrated in Pass 1 (e.g., due to a rate limit failure), Pass 2 will produce a 400 validation error for any item referencing it. Your mapping store should flag unmapped IDs before Pass 2 begins.


API Rate Limits: Exact Constraints and Performance Math

Both platforms enforce strict rate limits. A brute-force script will fail immediately.

Notion API Rate Limits

Notion enforces an average of 3 requests per second (RPS). Exceeding this returns 429 Too Many Requests. Your script must implement exponential backoff: retry after 1s, then 2s, then 4s, up to a configured maximum.

Notion paginates block children at 100 blocks per request. A page with 350 blocks requires 4 sequential API calls to fully read. A database with 500 rows requires 5 calls just for the row index, before fetching any page content.

Webflow API Rate Limits (v1 vs. v2)

Webflow's rate limits differ by API version. The v2 API (current as of 2024) applies per-endpoint rate limits rather than a single site-wide limit.

API Version Standard/CMS Plans Enterprise Plans Notes
v1 (legacy) 60 req/min (site-wide) 120 req/min Deprecated. Avoid for new scripts.
v2 (current) 60 req/min per endpoint Contact Webflow Limits apply per endpoint, not globally. Parallel writes to different endpoints do not share a bucket.

Webflow also enforces CMS item limits by plan:

Webflow Plan CMS Item Limit
Basic 0 (no CMS)
CMS 2,000
Business 10,000
Enterprise Custom (negotiated)

If your Notion database exceeds the item limit for your target Webflow plan, you must either upgrade or prune data before migration. There is no workaround; the Webflow API returns a 429-equivalent rejection when the collection is at capacity.

Realistic Migration Time Estimates

Given the stated rate limits, the wall-clock time for a migration is deterministic. Here is the math for three representative workloads:

Scenario A: 500 pages, 1,000 images, no relations

  • Notion reads: ~500 pages × ~2 API calls (index + blocks) = ~1,000 calls at 3 RPS ≈ 6 minutes
  • Image uploads to Webflow: 1,000 calls at 60/min ≈ 17 minutes
  • Webflow CMS item writes: 500 calls at 60/min ≈ 9 minutes
  • Estimated total: ~32 minutes (excluding processing time and backoff retries)

Scenario B: 2,000 pages, 5,000 images, two-pass relations (500 parent items)

  • Notion reads: ~2,000 pages × 3 avg calls = ~6,000 calls at 3 RPS ≈ 33 minutes
  • Image uploads: 5,000 calls at 60/min ≈ 83 minutes
  • Pass 1 CMS writes (parents): 500 calls at 60/min ≈ 9 minutes
  • Pass 2 CMS writes (children): 2,000 calls at 60/min ≈ 34 minutes
  • Estimated total: ~160 minutes (~2.7 hours)

Scenario C: 5,000 pages, 10,000 images, three-level relation hierarchy

  • Notion reads: ~5,000 pages × 3 avg calls = ~15,000 calls at 3 RPS ≈ 83 minutes
  • Image uploads: 10,000 calls at 60/min ≈ 167 minutes
  • CMS writes (three passes): 5,500 calls at 60/min ≈ 92 minutes
  • Estimated total: ~342 minutes (~5.7 hours)

These estimates assume no retries from 429 errors. In practice, add 15–30% for backoff delays on large migrations.


Webflow API Error Taxonomy

When a migration item fails, Webflow returns a structured 400 Bad Request response. Understanding the error shape lets you triage failures without stopping the entire pipeline.

Common Webflow CMS API error response shape (v2):

{
  "code": 400,
  "externalReference": null,
  "message": "Validation Failure",
  "details": [
    {
      "param": "fieldData.post-body",
      "expected": "string (valid HTML)",
      "received": "string with invalid tags"
    }
  ]
}

Common error causes and fixes:

Error Message / Cause Likely Field Fix
Validation Failure on Rich Text field richText field HTML contains disallowed tags or unclosed elements. Run sanitizer.
Invalid reference item ID Reference/Multi-Reference Target item does not exist in Webflow yet. Check Pass 1 completed successfully.
Item limit exceeded Collection Plan item limit reached. Upgrade plan or prune collection.
Field is required Name or required custom field Notion source has a blank Title or required field. Backfill before migration.
Option does not exist Select / Multi-Select Option value exists in Notion but was not pre-created in Webflow schema. Pre-seed all option values.
Invalid date format Date field Notion date string not converted to ISO 8601. Normalize before pushing.
Asset not found Image field Asset upload to Webflow Assets API failed silently. Re-run asset upload step for failed items.

Your migration script should log all 400 responses with the full response body and the source Notion page ID to a separate error file. Do not halt the script on a single item failure—continue and remediate failed items in a separate pass.


Migration Methods: Decision Framework

Choose your approach based on page volume, richness of block content, and presence of relational data.

1. Manual CSV Export / Import

Use when: Flat database only, no rich text body, no images, under 500 rows.

Notion's CSV export preserves property values but discards all page-body block content. Images export as temporary S3 URLs that will break within one hour. Relations export as plain text. This method is appropriate only for simple reference data (e.g., a tag list or author roster) where the page body is irrelevant.

2. Middleware Sync Tools (Make.com, Zapier, Whalesync)

Use when: Ongoing sync of low-volume, low-complexity data (under 200 items/month).

These tools handle API authentication and basic field mapping. Key limitations at migration scale:

  • Cost: A one-off migration of 3,000 pages through Make.com consumes approximately 9,000–15,000 operations depending on the scenario map, which typically exceeds the monthly quota of mid-tier plans.
  • Image handling: Most middleware tools pass Notion S3 URLs directly without downloading and re-hosting them. The 1-hour expiration issue is not solved.
  • Error visibility: Rich text validation failures from Webflow often surface as generic "item creation failed" errors with no field-level detail. Debugging is manual and slow.
  • No two-pass logic: These tools do not natively support the dependency-ordered two-pass relational migration pattern. Relations typically require manual post-migration linking.

3. Custom API Pipeline (Node.js or Python)

Use when: Volume exceeds 500 pages, content includes rich text or images, or relational fields are present.

A custom script gives you precise control over the transformation layer, error handling, and rate throttling. The complete pipeline involves: Notion SDK query → block extraction → Markdown conversion → HTML sanitization → image download/upload → two-pass CMS write with ID mapping. This is the only approach that reliably handles all Notion property types and guarantees image permanence.


Step-by-Step API Migration Workflow

Step 1: Audit and Normalize Notion Data

Before extracting anything, resolve the schema issues that will cause migration failures:

  • Identify all Formula, Rollup, People, and Created By fields. Decide whether to compute them to static values or drop them.
  • Ensure every database row has a non-empty Title property (maps to Webflow's required Name field).
  • Pre-create all Select and Multi-Select option values in your Webflow Collection schema. The Webflow API will reject items with option values that do not exist in the Collection's field definition.
  • Archive stale rows. Migrating dead content to Webflow consumes CMS item quota permanently.

Step 2: Extract Notion Data via API

Use the @notionhq/client SDK to query databases. Implement a pagination loop using Notion's next_cursor response field to handle databases over 100 rows. Store raw JSON responses to disk before any transformation. Decoupling extraction from transformation means you can re-run the transformation step without consuming Notion API quota again.

async function extractAllPages(databaseId) {
  const pages = [];
  let cursor = undefined;
  do {
    const response = await notion.databases.query({
      database_id: databaseId,
      start_cursor: cursor,
      page_size: 100,
    });
    pages.push(...response.results);
    cursor = response.has_more ? response.next_cursor : undefined;
  } while (cursor);
  return pages;
}

Step 3: Transform Block Arrays to Validated HTML

For each page, fetch block children (paginated at 100 blocks per request). Convert via the Notion JSON → Markdown → HTML pipeline described above. Apply the sanitizeForWebflow function. Parse the sanitized HTML for <img> tags pointing to s3.us-west-2.amazonaws.com—these require the asset re-hosting step.

Step 4: Process Images and Attachments

For each S3 image URL found in the HTML or in File/Image property fields:

  1. GET the S3 URL and stream the response to a buffer.
  2. POST the buffer to https://api.webflow.com/v2/sites/{site_id}/assets with the correct Content-Type header.
  3. Parse the response for the hostedUrl field (Webflow's CDN URL for the uploaded asset).
  4. Replace the S3 URL in your HTML or field payload with the hostedUrl.

Do this step before writing any CMS items. Image upload failures should be logged and retried before proceeding.

Step 5: Execute Two-Pass CMS Writes

Push parent collections first. Capture returned id fields from each successful POST /collections/{collection_id}/items response. Write the notion_id → webflow_id mapping to a persistent store. Verify the mapping store is complete before starting Pass 2. Push child collections, injecting resolved webflow_id values into Reference field arrays. Log all 400 responses with source Notion IDs for remediation.


Known Failure Modes and Remediation

Failure Mode Cause Detection Fix
Images return 403 after publish Notion S3 URL used directly without re-hosting Check image src attributes for amazonaws.com Re-run image download/upload step; replace all S3 URLs
400 on Rich Text field Disallowed HTML tags in content Webflow details field in error response Add disallowed tags to sanitizer config
Reference field empty after migration Target item not yet in Webflow at time of write Missing webflow_id in mapping store Confirm Pass 1 completion; re-run failed parent writes
Select option rejected Option value not pre-created in Collection schema 400 with "Option does not exist" message Extract all unique option values from Notion first; create in Webflow schema before migration
Migration stalls at item ~60 Webflow rate limit hit 429 response Implement exponential backoff; reduce concurrency to 1 request/second for Webflow writes
Notion blocks missing from output Page exceeds 100 blocks; pagination not implemented Content visibly truncated Implement block pagination loop using next_cursor
Formula/Rollup fields blank Not computed; passed as null Empty fields in Webflow Pre-compute values in Notion API response before mapping
Duplicate items on re-run No idempotency check Duplicate entries in Webflow Collection Check mapping store before writing; skip items with existing webflow_id

Summary: What This Migration Actually Requires

Migrating from Notion to Webflow is a data pipeline engineering problem with five distinct technical challenges:

  1. Schema translation: Notion's flexible property model does not map 1:1 to Webflow's CMS schema. Formula, Rollup, People, and Created By fields require explicit degradation decisions before the pipeline runs.
  2. Block-to-HTML conversion: Notion's nested JSON block structure must be converted to Webflow-valid HTML via a Markdown intermediary and a tag sanitizer targeting Webflow's specific allowed-tag subset.
  3. Image permanence: All Notion image URLs are signed S3 URLs that expire in one hour. They must be downloaded and re-uploaded to the Webflow Assets API before any CMS item is written.
  4. Relational ordering: Multi-reference fields require a two-pass architecture where parent collection items are fully migrated and their Webflow Item IDs captured before child items are written.
  5. Rate limit compliance: At 3 RPS (Notion) and 60 RPM (Webflow v2), a 2,000-page migration with 5,000 images takes approximately 2.7 hours under ideal conditions. Exponential backoff on 429 errors and a persistent ID mapping store are required infrastructure, not optional enhancements.

Frequently Asked Questions

Can I export Notion to Webflow using CSV?
Yes, but it is highly limited. A CSV export will transfer basic text fields, but it strips all rich text formatting, drops page content, breaks relational links, and exports images as temporary URLs that will expire.
Why do my Notion images break in Webflow after an hour?
Notion hosts images on AWS and provides signed URLs that expire exactly 60 minutes after generation. To fix this, your migration script must download the image file and re-upload it to Webflow's asset manager before linking it in the CMS.
How do I handle Notion relations in Webflow?
Notion Relations map to Webflow Reference or Multi-Reference fields. You must use a two-pass migration: first migrate the parent items to get their Webflow IDs, then migrate the child items and inject those IDs into the reference fields.
What is the Notion API rate limit?
The Notion API allows an average of 3 requests per second. Exceeding this will trigger a 429 Too Many Requests error, requiring your migration script to implement exponential backoff.

More from our Blog