---
title: "Notion to PDF via API: The Single Batch Request Recipe"
slug: notion-to-pdf-via-api-the-single-batch-request-recipe
date: 2026-03-04
author: Yuvraj Muley
categories: [Product Updates, Guides]
excerpt: "Learn how to convert Notion pages to PDF programmatically using a single API request, bypassing the need for Puppeteer or complex recursive scripts."
tldr: "The official Notion API lacks a PDF export endpoint. Instead of building a microservice with headless browsers, use Truto's `batch-requests` to fetch, recurse, and render Notion pages to PDF in one JSON payload."
canonical: https://truto.one/blog/notion-to-pdf-via-api-the-single-batch-request-recipe/
---

# Notion to PDF via API: The Single Batch Request Recipe


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](https://truto.one/blog/declarative-pagination-system-in-truto-unified-real-time-api/) 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](https://developers.notion.com/reference/get-block-children) endpoint, Notion returns only the immediate children. To reconstruct a full page, you must:

1.  **Traverse the tree:** Recursively fetch children for every block that has `has_children: true`.
2.  **Handle pagination:** Each level of the tree might be paginated.
3.  **Parse the layout:** Figure out how to render column lists and nested tables.
4.  **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:
1.  **Context:** Fetches the page metadata (title) to name the file.
2.  **Recursion:** Fetches the page content and automatically traverses the block tree.
3.  **Spooling:** Aggregates all the paginated and recursive fragments into one dataset.
4.  **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.

```json
{
  "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.

```json
{
  "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]
> **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.

```json
{
  "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.

1.  **`combine-page-content`**: We use `$convertNotionToMarkdown` to turn the massive array of Notion blocks into a clean Markdown string. We also sort the nodes to ensure the document flow is correct.
2.  **`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 the `page_title` we saved in Step 1 for the filename.

```json
{
  "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`.

```bash
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](https://truto.one/blog/rag-simplified-with-truto/) where you might need clean Markdown instead of a PDF, or for archiving data from other hierarchical systems like SharePoint or Confluence.

> Need to orchestrate complex data flows from [Notion](https://truto.one/blog/introducing-our-notion-integration/), Salesforce, or Zendesk? Let's talk about how batch requests can simplify your stack.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
