---
title: "Ingest Notion Pages into LlamaIndex with EnConvert"
description: "Use EnConvert to ingest Notion pages into LlamaIndex as heading-aware chunks with page and section metadata, ready to load as Document objects and query"
canonical: "https://www.enconvert.com/solutions/ingest-notion-pages-into-llamaindex-with-enconvert"
locale: "en"
---

# How to Ingest Notion Pages into LlamaIndex with EnConvert

A developer building a LlamaIndex agent or RAG pipeline that reads Notion pages needs clean, heading-aware chunks with page and section metadata, not raw content from the Notion API that still needs parsing and chunking. The Notion API returns page content as a list of block objects such as paragraphs, headings, toggles, and callouts, which you have to reassemble into text, detect heading boundaries in, and chunk before anything loads into LlamaIndex. For an agent answering questions about product specs, internal wikis, or project documentation, raw block JSON is not a useful retrieval unit. EnConvert’s Ingest endpoint takes a publicly accessible Notion page URL and returns JSONL chunks split at H1 to H3 heading boundaries, each carrying its headings_path breadcrumb, section name, and page title in metadata, so Notion content is ready to load as LlamaIndex Document objects and retrieve in one call.

## Example

```python
import os
import json
import time

import requests
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.schema import TextNode
from llama_index.core.vector_stores import MetadataFilter, MetadataFilters

# LlamaIndex (pip install llama-index) uses OpenAI by default for embeddings and
# answers, so OPENAI_API_KEY must also be set in the environment
HEADERS = {"X-API-Key": os.environ["ENCONVERT_API_KEY"]}
PAGE_URL = os.environ["NOTION_URL"]  # a publicly shared Notion page URL

# Step 1: Submit the public Notion page URL
# Pages behind a login are skipped and the job fails with an error_message
submit = requests.post(
    "https://api.enconvert.com/v2/ingest",
    headers=HEADERS,
    json={"mode": "urls", "urls": [PAGE_URL]},
    timeout=60,
)
submit.raise_for_status()
job_id = submit.json()["job_id"]

# Step 2: Poll until the job completes
for _ in range(120):
    status_response = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers=HEADERS,
        timeout=30,
    )
    status_response.raise_for_status()
    job = status_response.json()
    if job["status"] == "completed":
        break
    if job["status"] in ("failed", "canceled"):
        raise RuntimeError(job.get("error_message") or f"Ingest job {job['status']}")
    time.sleep(5)
else:
    raise TimeoutError(f"Ingest job {job_id} did not finish within 10 minutes")

# Step 3: Download the JSONL output
# The signed URL expires after 15 minutes. Plain GET, no API key.
jsonl_response = requests.get(job["output_url"], timeout=60)
jsonl_response.raise_for_status()

chunks = [json.loads(line) for line in jsonl_response.iter_lines() if line]
print(f"Loaded {len(chunks)} chunks ready for embedding")

# Step 4: Load each chunk as a LlamaIndex Document with metadata preserved
# Chunks split at H1 to H3 boundaries, 512 word soft cap
# Tables and code blocks kept whole even when they exceed the cap
documents = []
for chunk in chunks:
    meta = chunk["metadata"]
    documents.append(
        Document(
            id_=chunk["id"],
            text=chunk["content"],
            metadata={
                "source_url": meta["source_url"],
                "title": meta["title"],
                "section": meta["section"],
                # headings_path is a list; metadata values stay plain strings
                "headings_path": " > ".join(meta["headings_path"]),
                "chunk_index": meta["chunk_index"],
                "word_count": meta["word_count"],
            },
            # Embed the text with title and section only
            excluded_embed_metadata_keys=[
                "chunk_index",
                "word_count",
                "source_url",
                "headings_path",
            ],
        )
    )

# Step 5: Convert to TextNodes so LlamaIndex indexes the EnConvert chunks as they
# are. VectorStoreIndex.from_documents would re-split them.
nodes = [
    TextNode(
        id_=doc.id_,
        text=doc.text,
        metadata=doc.metadata,
        excluded_embed_metadata_keys=doc.excluded_embed_metadata_keys,
    )
    for doc in documents
]

# Step 6: Build the vector index
index = VectorStoreIndex(nodes=nodes)

# Step 7: Query narrowed to this page by a metadata filter
# source_url is the exact URL you submitted
query_engine = index.as_query_engine(
    filters=MetadataFilters(
        filters=[MetadataFilter(key="source_url", value=PAGE_URL)]
    )
)
# Replace the question with one your own page can answer
print(query_engine.query("What are the launch requirements in this spec?"))
```

## Output

Ingest returns the Notion page as JSONL chunks split at H1 to H3 heading boundaries, with tables and code blocks kept whole even when they exceed the 512 word default soft cap, each carrying its headings_path breadcrumb, section name, and page title, so LlamaIndex retrieval can filter by page or section. The Notion API returns the same page as a list of block objects you have to reassemble, detect heading boundaries in, and chunk before any content reaches LlamaIndex. This works for publicly accessible Notion pages only; pages that require a login or a Notion integration token use a different workflow.

## Get started

[Get free API key — 500 ops per month, no card required.](https://www.enconvert.com/auth?mode=signup)

## Related solutions

- [How to Ingest Markdown Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-files-into-llamaindex-with-enconvert.md)
- [How to Ingest Web Pages into Vertex AI with EnConvert](https://www.enconvert.com/solutions/ingest-web-pages-into-vertex-ai-with-enconvert.md)
- [How to Ingest Markdown into OpenAI File Search with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-into-openai-file-search-enconvert.md)
- [How to Ingest HTML Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-html-files-into-llamaindex-with-enconvert.md)
- [Best Browserless Alternative for Document Parsing](https://www.enconvert.com/solutions/browserless-alternative-for-document-parsing-enconvert.md)
- [How to Ingest DOCX into CrewAI with EnConvert](https://www.enconvert.com/solutions/ingest-docx-into-crewai-with-enconvert.md)
