Automate a Notion-to-CMS publishing pipeline: subscribe to page.updated webhooks, then recursively fetch all blocks with GET /v1/blocks/{id}/children (100 blocks per page, must recurse for nested content), transform block types to HTML or Markdown, and push to an external CMS API. Critical: image block URLs expire in ~1 hour — download and re-upload images immediately. A 30-block article with toggles can require 10+ paginated block-children requests. Rate limit: 3 req/s.
API Quick Reference
Bearer Token
3 requests/second
JSON
Available
Understanding the Notion API
The Notion REST API uses a block-tree model for page content. Every piece of content in Notion is a block: paragraph, heading_1/2/3, code, image, quote, callout, toggle, bulleted_list_item, numbered_list_item, divider, bookmark, table, column_list, and more. GET /v1/blocks/{id}/children returns only one level deep with a maximum of 100 blocks per page — nested content (toggle children, column contents, table rows) requires recursive follow-up calls.
For content publishing, the automation must: detect when a page's status changes to 'Ready to Publish' via a webhook or poll, recursively walk the entire block tree, transform each block type to the target CMS format (HTML, Markdown, or JSON), handle image blocks specially (download before URL expiry), and finally push to the external CMS API.
This is the most technically demanding Notion automation because of block-tree traversal complexity. Official documentation: developers.notion.com.
https://api.notion.comSetting Up Notion API Authentication
The content publishing pipeline needs Read content to fetch blocks, Update content to mark pages as 'Published', and webhook subscription access using Notion-Version: 2026-03-01. The webhook fires on page.updated events when a content author changes the status to 'Ready to Publish'.
- 1Go to notion.so/my-integrations (workspace owner required)
- 2Create or open your integration and enable: Read content, Update content
- 3Copy the Internal Integration Secret (ntn_ prefix)
- 4Share your content database with the integration via '...' > Connections
- 5Set up a public HTTPS endpoint for webhook delivery
- 6Create webhook subscription (POST /v1/webhooks with Notion-Version: 2026-03-01) for page.updated events
- 7Store credentials: NOTION_TOKEN, NOTION_WEBHOOK_SECRET, CONTENT_DB_DS_ID
1import os2import requests34NOTION_TOKEN = os.environ['NOTION_TOKEN']56HEADERS = {7 'Authorization': f'Bearer {NOTION_TOKEN}',8 'Notion-Version': '2025-09-03',9 'Content-Type': 'application/json'10}1112WEBHOOK_HEADERS = {13 'Authorization': f'Bearer {NOTION_TOKEN}',14 'Notion-Version': '2026-03-01', # required for webhook endpoints15 'Content-Type': 'application/json'16}Security notes
- •Store NOTION_TOKEN and NOTION_WEBHOOK_SECRET in environment variables
- •Image URLs expire in ~1 hour — download and store images immediately, never cache Notion file URLs
- •Verify X-Notion-Signature on every incoming webhook before processing
- •The webhook verification_token is your signing secret — treat it like a password
- •Never log raw page content that may contain sensitive draft material
Key endpoints
/v1/blocks/{block_id}/childrenFetch child blocks of a page or block. Returns max 100 blocks per page with cursor-based pagination. Must be called recursively for nested blocks (toggles, column_lists, table_rows). This is the core endpoint for extracting article content.
| Parameter | Type | Required | Description |
|---|---|---|---|
block_id | string | required | The page ID or block ID whose children to fetch. Use the page ID to start the recursive walk. |
page_size | number | optional | Max 100 blocks per request. |
start_cursor | string | optional | Cursor from previous response for pagination within the same block's children. |
Response
1{"object":"list","results":[{"id":"block-id-1","type":"heading_2","heading_2":{"rich_text":[{"plain_text":"Introduction"}]}},{"id":"block-id-2","type":"paragraph","paragraph":{"rich_text":[{"plain_text":"Welcome to this guide..."}]}},{"id":"block-id-3","type":"toggle","has_children":true,"toggle":{"rich_text":[{"plain_text":"Show more details"}]}}],"next_cursor":null,"has_more":false}/v1/pages/{page_id}Fetch a page's properties — used to check the status property value and get article metadata (title, author, publish date, featured image relation).
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | required | UUID of the Notion page to retrieve. |
Response
1{"object":"page","id":"page-id","properties":{"Title":{"title":[{"plain_text":"How to Deploy on Vercel"}]},"Status":{"select":{"name":"Ready to Publish"}},"Author":{"people":[{"name":"Alice"}]},"Publish Date":{"date":{"start":"2026-05-07"}}}}/v1/webhooksSubscribe to page.updated events for the content database. Requires Notion-Version: 2026-03-01. Use this to get notified when a page status changes to 'Ready to Publish' without polling.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | required | HTTPS endpoint for webhook delivery. |
events | array | required | ['page.updated'] for status change detection. |
resource | object | required | {type: 'database', id: 'DB_ID'}. |
Request
1{"url":"https://yourapp.com/webhooks/notion","events":["page.updated"],"resource":{"type":"database","id":"CONTENT_DB_ID"}}Response
1{"id":"webhook-id","verification_token":"verify_token","active":false}/v1/pages/{page_id}Update the page status to 'Published' after successful CMS push, creating a clear audit trail in Notion.
| Parameter | Type | Required | Description |
|---|---|---|---|
properties | object | optional | Properties to update. Only include properties you want to change. |
Request
1{"properties":{"Status":{"select":{"name":"Published"}}}}Response
1{"object":"page","id":"page-id","properties":{"Status":{"select":{"name":"Published"}}}}Step-by-step automation
Detect Status Change via Webhook
Why: Real-time webhook detection means articles are published within seconds of being marked ready, rather than waiting for a polling interval.
Configure a POST /v1/webhooks subscription for page.updated events on your content database. In the handler, verify the X-Notion-Signature, fetch the full page with GET /v1/pages/{id}, and check the Status property value. Only proceed if the status is exactly 'Ready to Publish'.
1# Create webhook subscription2curl -X POST 'https://api.notion.com/v1/webhooks' \3 -H 'Authorization: Bearer $NOTION_TOKEN' \4 -H 'Notion-Version: 2026-03-01' \5 -H 'Content-Type: application/json' \6 -d '{"url":"https://yourapp.com/webhooks/notion","events":["page.updated"],"resource":{"type":"database","id":"CONTENT_DB_ID"}}'Pro tip: Webhook payloads do not tell you which property changed — they only say the page was updated. You must fetch the page and check the status on every page.updated event, even if the status did not change. Cache the last known status per page ID to avoid re-publishing if the webhook fires multiple times.
Expected result: The webhook handler returns 200 immediately. When a page status changes to 'Ready to Publish', the publishing pipeline starts asynchronously.
Recursively Fetch All Blocks
Why: GET /v1/blocks/{id}/children only returns one level deep — nested content inside toggles, column lists, and tables requires recursive calls.
Start with the page ID, fetch its children with pagination, then recursively fetch children for any block where has_children is true. Block types requiring recursion: toggle, column_list (then each column), table (then each table_row), synced_block, template, and some callout blocks. Images and other leaf blocks have has_children: false and do not need recursion.
1# Fetch top-level page blocks2curl -X GET 'https://api.notion.com/v1/blocks/PAGE_ID/children?page_size=100' \3 -H 'Authorization: Bearer $NOTION_TOKEN' \4 -H 'Notion-Version: 2025-09-03'56# Fetch nested children of a toggle block7curl -X GET 'https://api.notion.com/v1/blocks/TOGGLE_BLOCK_ID/children?page_size=100' \8 -H 'Authorization: Bearer $NOTION_TOKEN' \9 -H 'Notion-Version: 2025-09-03'Pro tip: Skip recursion for 'child_page' and 'child_database' blocks — these are references to other Notion pages and databases, not inline content. Including them would cause infinite recursion if the article has database embeds.
Expected result: A flat array of all blocks in the article, including nested content with _depth metadata. Each block has its type and the type-specific content object.
Download Images Before URL Expiry
Why: Notion image URLs expire in approximately 1 hour — if you wait until the HTML transformation step to download them, they may have already expired.
Scan the block list for image blocks immediately after fetching. Download each image URL to your server or S3 bucket and replace the Notion URL with your permanent URL before proceeding to transformation. Image blocks contain a file.url or external.url field depending on whether the image was uploaded to Notion or linked from an external source.
1# Download a Notion image (the URL is time-limited)2curl -L 'NOTION_IMAGE_URL_HERE' -o 'article-image-1.jpg'34# Then upload to your CDN/S3 and use the permanent URL in your HTMLPro tip: In production, upload images to S3 or Cloudflare R2 immediately after download and store the permanent public URL. Your CMS will need a permanent URL, not a local file path.
Expected result: All image blocks have a _local_image_path or permanent CDN URL attached. Image downloads happen before Notion URLs expire.
Transform Blocks to HTML and Push to CMS
Why: Each Notion block type maps to a specific HTML element — the transformer converts the Notion block tree into the markup your CMS expects.
Iterate through the flat block list and convert each block type to HTML or Markdown. Paragraph → p, heading_2 → h2, code → pre/code, image → img, bulleted_list_item → li, etc. Rich text within blocks can have annotations (bold, italic, code, link, color) that map to HTML inline elements. Finally, POST the transformed content to your CMS API (WordPress, Webflow, or a headless CMS).
1# Push transformed HTML to WordPress2curl -X POST 'https://yoursite.com/wp-json/wp/v2/posts' \3 -H 'Authorization: Basic BASE64_USER_APPPASS' \4 -H 'Content-Type: application/json' \5 -d '{"title":"Article Title","content":"<p>Your HTML content here</p>","status":"publish"}'Pro tip: Notion's rich text can contain color annotations and Notion-specific mention types. HTML has no equivalent for Notion colors — strip them or map to CSS classes. Mention blocks (@user, @page, @date) appear as plain text in plain_text — decide whether to render them as links or strip them.
Expected result: A valid HTML string containing the article content. Use this as the 'content' field when pushing to WordPress, Webflow, or any headless CMS API.
Complete working code
This complete script is a Flask webhook handler that receives Notion page.updated events, detects 'Ready to Publish' status, recursively fetches all blocks, downloads images, transforms to HTML, pushes to WordPress via REST API, and marks the Notion page as 'Published'.
1import os2import hmac3import hashlib4import time5import threading6import requests7from flask import Flask, request, jsonify89app = Flask(__name__)1011NOTION_TOKEN = os.environ['NOTION_TOKEN']12WEBHOOK_SECRET = os.environ['NOTION_WEBHOOK_SECRET']13WP_BASE = os.environ['WP_BASE_URL'] # e.g. https://yoursite.com/wp-json/wp/v214WP_AUTH = (os.environ['WP_USERNAME'], os.environ['WP_APP_PASSWORD'])1516H = {'Authorization': f'Bearer {NOTION_TOKEN}', 'Notion-Version': '2025-09-03'}1718def verify_sig(body, sig):19 d = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()20 return hmac.compare_digest(f'sha256={d}', sig)2122def notion_get(path):23 for _ in range(5):24 r = requests.get(f'https://api.notion.com{path}', headers=H)25 if r.status_code == 429:26 time.sleep(int(r.headers.get('Retry-After', 5)))27 continue28 r.raise_for_status()29 time.sleep(0.35)30 return r.json()31 raise Exception(f'Max retries for {path}')3233def fetch_children(block_id):34 blocks, cursor = [], None35 while True:36 params = {'page_size': 100}37 if cursor: params['start_cursor'] = cursor38 r = requests.get(f'https://api.notion.com/v1/blocks/{block_id}/children', headers=H, params=params)39 if r.status_code == 429:40 time.sleep(int(r.headers.get('Retry-After', 5)))41 continue42 r.raise_for_status()43 data = r.json()44 blocks.extend(data['results'])45 if not data['has_more']: break46 cursor = data['next_cursor']47 time.sleep(0.35)48 return blocks4950def fetch_all(block_id, depth=0):51 if depth > 5: return []52 result = []53 for block in fetch_children(block_id):54 block['_depth'] = depth55 result.append(block)56 if block.get('has_children') and block['type'] not in ['child_page', 'child_database']:57 result.extend(fetch_all(block['id'], depth + 1))58 return result5960def rich_to_html(arr):61 out = ''62 for rt in arr:63 txt = rt.get('plain_text', '')64 ann = rt.get('annotations', {})65 if ann.get('bold'): txt = f'<strong>{txt}</strong>'66 if ann.get('italic'): txt = f'<em>{txt}</em>'67 if ann.get('code'): txt = f'<code>{txt}</code>'68 if rt.get('href'): txt = f'<a href="{rt["href"]}">{txt}</a>'69 out += txt70 return out7172def to_html(blocks):73 parts = []74 for b in blocks:75 t = b['type']76 d = b.get(t, {})77 rt = d.get('rich_text', [])78 if t == 'paragraph': parts.append(f'<p>{rich_to_html(rt)}</p>')79 elif t in ('heading_1','heading_2','heading_3'):80 n = t[-1]81 parts.append(f'<h{n}>{rich_to_html(rt)}</h{n}>')82 elif t in ('bulleted_list_item','numbered_list_item'):83 parts.append(f'<li>{rich_to_html(rt)}</li>')84 elif t == 'code':85 parts.append(f'<pre><code>{rich_to_html(rt)}</code></pre>')86 elif t == 'image':87 src = b.get('_img_url', '#')88 parts.append(f'<img src="{src}" alt="">')89 elif t == 'quote':90 parts.append(f'<blockquote>{rich_to_html(rt)}</blockquote>')91 elif t == 'divider': parts.append('<hr>')92 return '\n'.join(parts)9394def publish_page(page):95 page_id = page['id']96 title = page['properties'].get('Title', {}).get('title', [])97 title_text = title[0]['plain_text'] if title else 'Untitled'98 blocks = fetch_all(page_id)99 # Download images100 for b in blocks:101 if b['type'] == 'image':102 img = b['image']103 url = img['file']['url'] if img['type'] == 'file' else img['external']['url']104 try:105 r = requests.get(url, timeout=30)106 b['_img_url'] = url # In prod: upload to S3, use permanent URL107 except: b['_img_url'] = '#'108 html = to_html(blocks)109 # Push to WordPress110 wp_resp = requests.post(f'{WP_BASE}/posts', auth=WP_AUTH, json={111 'title': title_text, 'content': html, 'status': 'publish'112 })113 wp_resp.raise_for_status()114 print(f'Published to WordPress: {wp_resp.json()["link"]}')115 # Mark Notion page as Published116 requests.patch(f'https://api.notion.com/v1/pages/{page_id}', headers={**H, 'Content-Type': 'application/json'},117 json={'properties': {'Status': {'select': {'name': 'Published'}}}})118119@app.route('/webhooks/notion', methods=['POST'])120def webhook():121 raw = request.get_data()122 if not verify_sig(raw, request.headers.get('X-Notion-Signature', '')):123 return jsonify({'error': 'bad sig'}), 401124 p = request.json125 page_id = p.get('entity', {}).get('id')126 if page_id:127 page = notion_get(f'/v1/pages/{page_id}')128 if page.get('properties', {}).get('Status', {}).get('select', {}).get('name') == 'Ready to Publish':129 threading.Thread(target=publish_page, args=(page,), daemon=True).start()130 return jsonify({'ok': True})131132if __name__ == '__main__':133 app.run(port=3000)Error handling
{"object":"error","status":400,"code":"missing_version","message":"Notion-Version header is required."}The Notion-Version header is missing. The webhook subscription endpoint requires version 2026-03-01; all other endpoints use 2025-09-03.
Maintain two header objects: one with Notion-Version: 2025-09-03 for page/block calls and one with 2026-03-01 for webhook management.
No retry — fix the header.
Access Denied (from AWS S3 when accessing expired Notion image URL)Notion image file URLs expire after approximately 1 hour. If you try to download or render the URL after expiry, S3 returns 403.
Download images immediately after fetching blocks, before any transformation or waiting. In the webhook handler, image download must be the first operation after block fetching.
Re-fetch the page and blocks to get fresh URLs if images have expired. Set a 45-minute maximum between block fetching and image processing.
{"object":"error","status":404,"code":"object_not_found","message":"Could not find block with ID: ..."}A block that has_children was deleted between the parent fetch and the children fetch, or the page was not shared with the integration.
Handle 404 gracefully during recursive block fetching — skip blocks that return 404 and continue with siblings. Log the block ID for debugging.
Do not retry 404 — skip and continue.
{"object":"error","status":429,"code":"rate_limited"}A 30-block article with 3 nested toggles requires at least 4 paginated API calls just to fetch blocks, plus additional calls for images and metadata.
Add 350ms delays between every block-children call and every recursive fetch. Honor Retry-After header if throttled.
Sleep for Retry-After seconds, then retry the specific failed call.
WordPress REST API error: Sorry, you are not allowed to publish posts.The WordPress Application Password belongs to an Author-level user who does not have publish_posts capability. Only Editor and Administrator roles can publish directly.
Use a WordPress Application Password belonging to an Editor or Administrator account. Authors can only create drafts (status: 'draft').
No retry — fix the WordPress user role.
Rate Limits for Notion API
| Scope | Limit | Window |
|---|---|---|
| Per integration token | 3 requests average | per second |
| API calls per article publish | 10-20+ requests | per article (block pages + recursive children + metadata + status update) |
| Block children per request | 100 blocks | per request |
1import time2import requests34def notion_get_with_retry(url, headers, params=None, max_retries=5):5 for attempt in range(max_retries):6 resp = requests.get(url, headers=headers, params=params)7 if resp.status_code == 429:8 wait = int(resp.headers.get('Retry-After', 2 ** attempt))9 print(f'Rate limited. Waiting {wait}s...')10 time.sleep(wait)11 continue12 resp.raise_for_status()13 time.sleep(0.35) # proactive buffer14 return resp.json()15 raise Exception(f'Max retries for {url}')- Add 350ms delays between every block-children call and every recursive child fetch
- Depth-limit the recursion to 5 levels — articles with deeper nesting are extremely rare and unlimited recursion is a denial-of-service risk
- Download images in the block loop, not a separate subsequent loop — minimizes time between URL issuance and download
- Process only one article at a time — concurrent publishing pipelines multiply the API calls and hit the rate limit fast
- Cache block metadata (has_children flag) to skip unnecessary recursive calls for leaf blocks
Security checklist
- Verify X-Notion-Signature on every webhook using HMAC-SHA256 before processing any content
- Download Notion image URLs immediately — they expire and become inaccessible after ~1 hour
- Store image files with non-guessable names (use the Notion block UUID as the filename) to prevent enumeration
- WordPress Application Passwords should belong to a dedicated automation account (Editor role), not your personal admin account
- Store NOTION_TOKEN, NOTION_WEBHOOK_SECRET, and WP_APP_PASSWORD in environment variables only
- Sanitize HTML output before sending to the CMS to prevent XSS from rich text link href values
- Never log full page content in your webhook handler — it may contain unpublished drafts
Automation use cases
Notion-to-WordPress Publisher
advancedDetect 'Ready to Publish' status in Notion, extract all blocks, transform to HTML, upload to WordPress with featured image, and mark the Notion page as Published.
Notion-to-Markdown Export
intermediateTransform Notion blocks to Markdown format and commit to a GitHub repository, enabling static site generators (Jekyll, Hugo, Astro) to pick up the new content.
Multi-CMS Syndication
advancedAfter publishing to WordPress, also push to a Webflow CMS collection and a headless CMS (Contentful, Sanity), with per-destination field mapping for each CMS's schema.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier available; paid plans from $19.99/monthZapier can detect Notion database row updates and create WordPress posts, though it cannot perform recursive block fetching or image downloading natively.
- + No code required for simple text content
- + WordPress integration included
- + Handles scheduling and retries
- - Cannot recursively fetch nested Notion blocks
- - No image download/re-upload capability
- - Rich text formatting is lost
Make
Free tier available; paid plans from $9/monthMake's Notion and WordPress modules support basic content transfers, with iterators for processing multiple blocks, though recursive traversal requires custom workarounds.
- + More powerful than Zapier for multi-step flows
- + Iterator module processes block arrays
- + Lower cost at scale
- - Cannot recurse into nested blocks natively
- - Image expiry not handled automatically
- - Complex mappings require manual JSON setup
n8n
Free self-hosted; cloud from €20/monthn8n's Code node can implement the full recursive block fetching and transformation logic, making it the most capable no-code alternative for this advanced automation.
- + Code node handles arbitrary recursion
- + Self-hostable
- + Webhook node for real-time triggers
- - Requires JavaScript knowledge in the Code node
- - Not truly no-code for complex block traversal
- - Notion API version support may lag
Best practices
- Download image URLs immediately after fetching blocks — they expire in ~1 hour and you cannot re-fetch them without another API call
- Depth-limit recursive block fetching to 5 levels as a safety guard against pathological nesting
- Never re-process a 'Ready to Publish' page that has already been published — check for a 'Published' status before starting the pipeline
- Store the WordPress post URL back in the Notion page after publishing using PATCH /v1/pages/{id} with a URL property
- Handle unknown block types gracefully — skip them and log the type so you can add support later
- Return 200 from the webhook handler immediately and process the full pipeline asynchronously
- Use the Notion block ID as the image filename to create a deterministic mapping between Notion content and CMS media
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Notion-to-WordPress content pipeline using the Notion API v2025-09-03. My recursive block fetching function calls GET /v1/blocks/{id}/children and recurses on blocks where has_children is true. The function works for flat articles but enters an infinite loop on some pages. Here is my recursion code: {paste code here}. The problematic page has child_database blocks. How do I prevent infinite recursion and which block types should I skip?
Build a Next.js content publishing dashboard that connects to Notion and WordPress. The app should show all Notion database pages with a 'Ready to Publish' status in a table (fetched via POST /v1/data_sources/{id}/query). Each row should have a 'Publish Now' button that: fetches the full page metadata with GET /v1/pages/{id}, recursively fetches all blocks via GET /v1/blocks/{id}/children, transforms blocks to HTML (paragraph to p, heading_2 to h2, code to pre/code, image to img), POSTs to WordPress /wp-json/wp/v2/posts using Application Password auth, and updates the Notion page status to 'Published' via PATCH /v1/pages/{id}. Show a progress indicator during publishing and display the WordPress post URL when done. Use NOTION_TOKEN and WordPress credentials from environment variables.
Frequently asked questions
Why do I get 403 errors when accessing Notion image URLs?
Notion stores images on AWS S3 with time-limited signed URLs that expire after approximately 1 hour. After expiry, S3 returns 403 Access Denied. The solution is to download images immediately after fetching the blocks that contain them — never wait, never cache the URL. In production, upload the downloaded image to your own CDN or S3 bucket and use the permanent URL in your CMS.
How many API calls does it take to publish a typical blog post?
A 20-block article without nesting requires approximately 4 calls: 1 webhook trigger follow-up (GET /v1/pages/{id}), 1-2 block children pages (GET /v1/blocks/{id}/children), and 1 status update (PATCH /v1/pages/{id}). A 50-block article with 5 nested toggles and a table requires 8-12 calls just for block fetching. Each image download is an additional external HTTP call (not rate-limited by Notion).
What block types require recursive fetching?
Any block where has_children is true requires a follow-up GET /v1/blocks/{id}/children call. Common types: toggle, column_list (then each column block), table (then each table_row), callout (sometimes), synced_block. Leaf blocks (paragraph, heading, image, code, divider) never have children. Skip recursion for child_page and child_database to avoid infinite loops.
Can I convert Notion blocks to Markdown instead of HTML?
Yes. Instead of wrapping content in HTML tags, use Markdown equivalents: # for heading_1, ## for heading_2, > for quote, ** ** for bold, * * for italic, ` ` for inline code, ```lang for code blocks, - for bulleted_list_item, 1. for numbered_list_item. The same block iteration and rich text extraction logic applies — just change the output format.
Is the Notion API free for content pipeline automation?
Yes. The Notion API is free with no paid tier. The only limitation is the 3 req/s rate limit, which is the same for all plan levels. The webhook system (used for real-time detection) is in public beta but free to use.
How do I handle Notion synced blocks in the pipeline?
Synced blocks (type: synced_block) have has_children: true and contain a synced_from object pointing to the original block ID. Fetch the children of the synced_block's own ID — Notion automatically returns the synced content. Do not follow the synced_from reference directly, as that points to the source block which may be in a different page.
What is the difference between Notion-Version: 2025-09-03 and 2026-03-11?
In version 2026-03-11, two things changed: 'archived' property on pages was renamed to 'in_trash', and the 'after' string parameter for PATCH /v1/blocks/{id}/children was replaced by a 'position' object. For read-only operations like block fetching and page reading, both versions work identically. Pin to 2025-09-03 for stability unless you need position-based block insertion.
Can RapidDev build a custom Notion content publishing pipeline?
Yes. RapidDev has built 600+ apps including automated content pipelines from Notion to WordPress, Webflow, and headless CMSes. We handle recursive block traversal, image CDN migration, and CMS field mapping. Visit rapidevelopers.com for a free consultation.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation