Notion to PDF via API: The Single Batch Request Recipe
Learn how to convert Notion pages to PDF programmatically using a single API request, bypassing the need for Puppeteer or complex recursive scripts.
If you have ever tried to programmatically export a Notion page to PDF, you have likely hit a wall. The official Notion API is excellent for data retrieval, but it explicitly lacks an export endpoint. It returns a tree of JSON blocks, not a document.
To get a PDF, the standard engineering solution is a heavy lift: you spin up a Node.js microservice, write a recursive script to fetch nested block children (handling pagination at every level), convert that JSON tree to Markdown or HTML, and then maintain a headless browser instance (like Puppeteer) to render the final binary. It is a lot of infrastructure for a simple file export.
There is a better way. By using Truto's batch-requests endpoint, you can define this entire fetch-transform-render pipeline in a single JSON payload. No headless browsers, no recursive scripts to maintain, and no infrastructure overhead.
Here is the recipe to convert Notion pages to PDF in one request.
The Problem: Notion's API Gives You Blocks, Not PDFs
Notion pages are not stored as linear documents; they are stored as a graph of Block objects. A page contains blocks, and those blocks (like columns, toggles, or synched blocks) can contain other blocks.
When you query the Retrieve block children endpoint, Notion returns only the immediate children. To reconstruct a full page, you must:
- Traverse the tree: Recursively fetch children for every block that has
has_children: true. - Handle pagination: Each level of the tree might be paginated.
- Parse the layout: Figure out how to render column lists and nested tables.
- Render: Pipe the resulting structure into a PDF generator.
Most teams solve this by building a dedicated service just to handle the "Notion to PDF" feature. We solved it by building a recursive orchestration engine into our API.
The Solution: A Single Batch Request
Truto's batch-requests endpoint allows you to chain multiple operations—API calls, logic flow, and data transformations—into one execution context.
We will construct a payload that performs four distinct actions:
- Context: Fetches the page metadata (title) to name the file.
- Recursion: Fetches the page content and automatically traverses the block tree.
- Spooling: Aggregates all the paginated and recursive fragments into one dataset.
- Rendering: Converts the JSON to Markdown, then renders that Markdown to PDF.
Step 1: The Setup (get-page-details & page-context)
First, we need to know what we are converting. We fetch the root page object to extract its title. We then store this title in a context variable so we can use it later to name the generated PDF file.
{
"name": "get-page-details",
"resource": "knowledge-base/pages",
"method": "get",
"id": "{{args.page_id}}"
},
{
"name": "page-context",
"type": "add_context",
"depends_on": "get-page-details",
"config": {
"expression": "{ \"page_title\": resources.`knowledge-base`.pages[0].title }"
}
}Step 2: Recursive Fetching (page-content)
This is where the heavy lifting happens. Instead of writing a client-side loop, we define a recurse configuration on the resource node.
We target the block-children resource. The recurse block tells Truto: "If the argument include_child_pages is true, look at the current block. If it has children, fetch them using the current block's ID as the parent."
This runs entirely on the server side. You don't handle the network latency of hundreds of round-trip requests.
{
"name": "page-content",
"resource": "block-children",
"method": "list",
"query": {
"block_id": "{{args.page_id}}",
"truto_ignore_remote_data": true
},
"recurse": {
"if": "{{args.include_child_pages:bool}}",
"config": {
"query": {
"block_id": "{{resources.block-children.id}}"
}
}
},
"persist": false
}Note: We set persist: false here because we don't need to store the raw JSON blocks in the database; we only need them in memory to generate the PDF.
Step 3: Aggregating Data (spool Node)
Because the previous step involves pagination and recursion, the data comes back in fragments (pages of blocks). If we tried to process this immediately, we would only get partial content.
The spool node acts as a buffer. It collects every single block returned by the page-content node—across all pages and recursion depths—and bundles them into a single array available to the next step.
{
"name": "all-page-content",
"type": "spool",
"depends_on": "page-content"
}Step 4: Transformation & Rendering
Finally, we apply two transformations using Truto's built-in JSONata functions.
combine-page-content: We use$convertNotionToMarkdownto turn the massive array of Notion blocks into a clean Markdown string. We also sort the nodes to ensure the document flow is correct.md-to-pdf: We pass that Markdown to$convertMdToPdf. This function handles the rendering, including syntax highlighting for code blocks, tables, and images. It uses thepage_titlewe saved in Step 1 for the filename.
{
"name": "md-to-pdf",
"type": "transform",
"config": {
"expression": "$convertMdToPdf(resources.`block-children`[0], {\"title\": `page-context`.page_title, \"filename\": `page-context`.page_title})"
},
"depends_on": "combine-page-content",
"persist": true
}The Complete JSON Recipe
Here is the full payload. You can POST this directly to https://api.truto.one/batch-requests.
Just replace YOUR_INTEGRATED_ACCOUNT_ID (the connection to the specific Notion workspace) and YOUR_PAGE_ID.
curl -X POST \
https://api.truto.one/batch-requests \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <YOUR_API_TOKEN>' \
-d '{
"integrated_account_id": "YOUR_INTEGRATED_ACCOUNT_ID",
"args": {
"page_id": "YOUR_PAGE_ID",
"include_child_pages": true,
"link_child_pages": false
},
"resources": [
{
"name": "get-page-details",
"resource": "knowledge-base/pages",
"method": "get",
"id": "{{args.page_id}}"
},
{
"name": "page-context",
"type": "add_context",
"depends_on": "get-page-details",
"config": {
"expression": "{ \"page_title\": resources.`knowledge-base`.pages[0].title, \"page_id": resources.`knowledge-base`.pages[0].id }"
}
},
{
"name": "page-content",
"resource": "block-children",
"method": "list",
"query": {
"block_id": "{{args.page_id}}",
"truto_ignore_remote_data": true
},
"recurse": {
"if": "{{args.include_child_pages:bool}}",
"config": {
"query": {
"block_id": "{{resources.block-children.id}}"
}
}
},
"persist": false
},
{
"name": "all-page-content",
"type": "spool",
"depends_on": "page-content"
},
{
"name": "combine-page-content",
"type": "transform",
"config": {
"expression": "$convertNotionToMarkdown($sortNodes($map(resources.`block-children`, function($v) { $merge([$v, {\"parent_id\": $firstNonEmpty($v.parent.page_id, $v.parent.block_id)}]) }), \"id\", \"parent_id\"), args.link_child_pages)"
},
"depends_on": "all-page-content",
"persist": false
},
{
"name": "md-to-pdf",
"type": "transform",
"config": {
"expression": "$convertMdToPdf(resources.`block-children`[0], {\"title\": `page-context`.page_title, \"filename\": `page-context`.page_title})"
},
"depends_on": "combine-page-content",
"persist": true
}
]
}'Why this approach wins
This recipe highlights the difference between a simple API wrapper and a Unified API platform. If you were to build this yourself, you would be managing a stack of notion-to-md libraries and a headless Chrome instance.
By pushing the recursion and rendering logic to the API layer, you treat the "Notion to PDF" problem as a data pipeline configuration rather than an infrastructure project. This same pattern works for RAG pipelines where you might need clean Markdown instead of a PDF, or for archiving data from other hierarchical systems like SharePoint or Confluence.
FAQ
- Does the Notion API have an export to PDF endpoint?
- No, the official Notion API only allows you to retrieve block data as JSON. It does not provide an endpoint to export pages as PDF, HTML, or Markdown directly.
- How do I handle nested blocks when fetching Notion pages?
- You must recursively call the 'Retrieve block children' endpoint for every block that contains children. Truto handles this automatically with the `recurse` configuration in batch requests.
- Can I convert Notion to Markdown using the API?
- Yes. The recipe above generates Markdown as an intermediate step. You can modify the final transform node to return the Markdown string instead of rendering a PDF.
- What is a spool node in Truto?
- A spool node is a utility in Truto's batch framework that aggregates data from paginated or recursive API calls into a single dataset for processing.