Notion

Notion API Integration

Published
December 26, 2025

Webflow's CMS is powerful but it has some limitations.

  • Creating and Updating items. Full capabilities are available however there is some inherent friction- you must log into Webflow and use the designer, which isn't the smoothest experience when editing e.g. large rich-text content.
  • Likewise, it pushes you through an item-level publishing process, where new changes are assumed to be "draft changes." This can work very well but often, you just want to make a change and have it published immediately with as few clicks as possible.
  • Rich-text capabilities are limited, e.g. no table support.

Notion's content block-structure is very appealing because it maps nicely to the structures in Webflow's rich-text elements.

Our Experience

Sygnal uses Notion for a lot of our content management and recently we've begin using it with direct Webflow integration to update our CMS content.

This KB section of our website is a new initiative which is entirely Notion-driven, with 100% of the content written in Notion and synced to the CMS.

Technology

What Notion offers

  • Webhooks (official): Subscribe to page/database events (e.g., page.content_updated, database.item.updated). Notion sends a POST to your endpoint; you then call the Notion API to fetch the new content. Notion Developers
  • API (official): Use the Notion API to get full data, including content blocks, and to perform content manipulations.
  • Manual Automations → "Send webhook" action: Setup a button, database button, or database-automation and Notion can POST to your URL when a page/property changes. Notion

High-level flow

  1. In Notion, enable either:
  2. Webhook target = your Cloudflare Worker URL. Worker verifies request and queues work.
  3. Worker calls Notion API to read page properties and blocks (latest content). Notion Developers
  4. Convert blocks → HTML (custom renderer or library).
  5. Call Webflow CMS API to create/update the mapped item; publish per current API rules. developers.webflow.com+1

Notes

  • Webhook payload is a signal (IDs, timestamps), not full content → you must fetch the page/blocks after receiving it. Notion Developers
  • Use page/database filters in Notion to limit which edits fire your automation.
  • Webflow: map fields (title, slug, rich text HTML, references), then publish the updated item. Review 2025 CMS publishing changes. developers.webflow.com+1

Minimal Worker sketch (TypeScript)

export default {
  async fetch(req: Request, env: Env) {
    // 1) Validate source (optionally check a shared secret header from Notion Automation)
    if (req.method !== "POST") return new Response("OK");
    const event = await req.json(); // contains page/database IDs

    // 2) Fetch latest from Notion
    const pageId = event.data?.id ?? event.payload?.page?.id; // handle both webhook/automation shapes
    const page = await fetch("https://api.notion.com/v1/pages/" + pageId, {
      headers: {
        "Authorization": `Bearer ${env.NOTION_TOKEN}`,
        "Notion-Version": "2022-06-28"
      }
    }).then(r=>r.json());

    const blocks = await fetch(`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`, {
      headers: {
        "Authorization": `Bearer ${env.NOTION_TOKEN}`,
        "Notion-Version": "2022-06-28"
      }
    }).then(r=>r.json());

    // 3) Convert Notion blocks -> HTML (implement renderBlocks(blocks.results))
    const html = renderBlocks(blocks.results);

    // 4) Upsert Webflow CMS item
    const siteId = env.WF_SITE_ID, collectionId = env.WF_COLLECTION_ID;
    const itemPayload = {
      isArchived: false,
      isDraft: false,
      fieldData: {
        name: page.properties?.Name?.title?.[0]?.plain_text ?? "Untitled",
        slug: slugify(page.properties?.Slug?.rich_text?.[0]?.plain_text ?? pageId),
        rich_content: html
      }
    };

    const upsert = await fetch(`https://api.webflow.com/v2/collections/${collectionId}/items`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.WEBFLOW_TOKEN}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(itemPayload)
    }).then(r=>r.json());

    // 5) Publish (check latest API behavior)
    await fetch(`https://api.webflow.com/v2/sites/${siteId}/publish`, {
      method: "POST",
      headers: { "Authorization": `Bearer ${env.WEBFLOW_TOKEN}`, "Content-Type": "application/json" },
      body: JSON.stringify({ publishItems: [{ collectionId, itemId: upsert.id }] })
    });

    return new Response("OK");
  }
}

Rich Text

Block → HTML conversion

  • Either write a small renderer (paragraph, headings, bulleted/numbered lists, callouts, images, code) or use an OSS converter and adapt output.
  • Preserve inline annotations (bold/italic/link), map Notion image/file URLs to permanent hosts if needed.

Technical Notes

Security & reliability

  • Use a shared secret header in Notion Automation; verify in Worker. Queue heavy work with waitUntil.
  • Idempotency: store a last_synced_ts per page to avoid duplicate publishes.
  • Rate limits: both Notion and Webflow have quotas; backoff and retry.

Alternatives

  • Zapier/Make: "Notion database item updated" → "Webflow CMS: Create/Update Item." Faster to start but less control. Zapier+2Zapier+2

This setup gives you page-change triggers from Notion and a deterministic CMS update into Webflow.

Table of Contents
Comments
Did we just make your life better?
Passion drives our long hours and late nights supporting the Webflow community. Click the button to show your love.