notionify
High-fidelity bidirectional Markdown ↔ Notion conversion and synchronization SDK for Python 3.10+.
Features
- Markdown → Notion — Parse Markdown and create Notion pages with headings, lists, code blocks, tables, math equations, images, and more
- Notion → Markdown — Export Notion pages back to clean Markdown
- Diff sync — Update pages incrementally with minimal API calls using LCS-based diffing
- Image pipeline — Automatic upload of local files, data URIs, and external URLs via the Notion file upload API
- Async support — Full async client for high-throughput workflows
- Typed errors — Every failure has a specific error class with actionable context
- Retry & rate limiting — Built-in exponential backoff, jitter, and rate-limit compliance
Installation
pip install notionify
Quick Start
Create a page from Markdown
from notionify import NotionifyClient
with NotionifyClient(token="ntn_xxxxx") as client:
result = client.create_page_with_markdown(
parent_id="<page-or-database-id>",
title="Meeting Notes",
markdown="# Agenda\n\n- Review Q4 goals\n- **Action items**\n\n```python\nprint('hello')\n```",
)
print(result.page_id)
Append content to an existing page
client.append_markdown(
target_id="<page-id>",
markdown="## New Section\n\nAppended content with *formatting*.",
)
Update a page with diff sync
Only the changed blocks are updated — unchanged content is left untouched:
result = client.update_page_from_markdown(
page_id="<page-id>",
markdown=updated_markdown,
strategy="diff", # default; use "overwrite" to replace everything
)
print(f"Kept {result.blocks_kept}, inserted {result.blocks_inserted}, "
f"deleted {result.blocks_deleted}")
Export a Notion page to Markdown
markdown = client.page_to_markdown("<page-id>", recursive=True)
print(markdown)
Async client
import asyncio
from notionify import AsyncNotionifyClient
async def main():
async with AsyncNotionifyClient(token="ntn_xxxxx") as client:
result = await client.create_page_with_markdown(
parent_id="<page-id>",
title="Async Page",
markdown="# Created asynchronously\n\nWith full image support.",
)
print(result.page_id)
asyncio.run(main())
Debug CLI
The notionify-cli command wraps the SDK for local conversion, Notion page
push/pull/sync flows, raw API inspection, and diff dry-runs. It is also
available as python -m notionify.cli.
CLI quickstart
# Convert Markdown to Notion blocks JSON without an API call
notionify-cli convert doc.md
# Preview a new page conversion without token, parent, or API calls
notionify-cli push doc.md --dry-run
# Create a new page under a page or database
NOTION_TOKEN=secret_xxx \
notionify-cli push doc.md --parent <parent_id> --title "Doc"
# Incrementally sync an existing page from Markdown
notionify-cli sync doc.md --page <page_id>
# Show the diff plan without applying it
notionify-cli diff doc.md --page <page_id>
# Pull a page back to markdown
notionify-cli pull <page_id> --out out.md
# Inspect a page's raw JSON
notionify-cli inspect <page_id> --children --json
Global flags can be placed before or after the subcommand:
notionify-cli --json --profile work inspect <page_id> --children
notionify-cli inspect <page_id> --children --json --profile work
Useful global flags:
--token TOKEN— Notion integration token-c, --config PATH— use a specific TOML config file--profile NAME— select a TOML section, defaults todefault-v/-vv— show progress or detailed diagnostics--json— emit machine-readable result/error payloads
CLI configuration
For one-off commands, use environment variables:
export NOTION_TOKEN="secret_xxx"
export NOTION_DEFAULT_PARENT="<page-or-database-id>"
notionify-cli push doc.md --title "Doc"
For repeated use, create ~/.notionify.toml:
[default]
token = "secret_xxx"
default_parent = "12345678-1234-1234-1234-123456789abc"
[work]
token = "secret_work_xxx"
default_parent = "abcdefab-cdef-abcd-efab-cdefabcdefab"
[personal]
token = "secret_personal_xxx"
default_parent = "11111111-2222-3333-4444-555555555555"
Then select profiles with --profile:
notionify-cli --profile work push docs/release.md --title "Release Notes"
notionify-cli --profile personal pull <page_id> --out notes.md
Use -c PATH for a project-local config file. The repository includes
examples/notionify.test.toml as a safe
template with placeholder tokens:
notionify-cli -c examples/notionify.test.toml --profile staging \
sync doc.md --page <page_id>
Token precedence is:
--token-c PATHselected profileNOTION_TOKEN~/.notionify.tomlselected profile, only when-cis not used
default_parent precedence is:
-c PATHselected profile~/.notionify.tomlselected profile, only when-cis not usedNOTION_DEFAULT_PARENT
When -c PATH is provided, notionify treats that file as explicit and does
not fall back to ~/.notionify.toml.
CLI command reference
# convert: Markdown -> Notion block JSON, no token required
notionify-cli convert doc.md --out blocks.json
notionify-cli convert doc.md --no-images
# push: create a new Notion page
notionify-cli push doc.md --parent <parent_id> --title "Doc"
notionify-cli push doc.md --parent <database_id> --parent-type database
notionify-cli push doc.md --upload-remote-images
notionify-cli push doc.md --dry-run --json
# sync: update an existing page
notionify-cli sync doc.md --page <page_id>
notionify-cli sync doc.md --page <page_id> --dry-run --json
# diff: alias for sync --dry-run
notionify-cli diff doc.md --page <page_id>
# pull: export Notion page Markdown
notionify-cli pull <page_id>
notionify-cli pull <page_id> --out out.md
notionify-cli pull <page_id> --json
# inspect: dump raw page JSON for debugging
notionify-cli inspect <page_id> --children --json
Configuration
All options are passed as keyword arguments to the client constructor:
client = NotionifyClient(
token="ntn_xxxxx",
math_strategy="equation", # "equation" | "code" | "latex_text"
image_upload=True, # auto-upload local/data-URI images
image_fallback="skip", # "skip" | "placeholder" | "raise"
image_base_dir="/safe/path", # restrict local image reads to this dir
enable_tables=True, # convert Markdown tables to Notion tables
retry_max_attempts=5, # max retries on transient failures
rate_limit_rps=3.0, # requests per second limit
timeout_seconds=30.0, # per-request timeout
)
See the API Reference for the full list of options.
Error Handling
All errors inherit from NotionifyError and carry structured context:
from notionify import NotionifyClient, NotionifyRateLimitError, NotionifyError
with NotionifyClient(token="ntn_xxxxx") as client:
try:
client.create_page_with_markdown(
parent_id="<page-id>",
title="Test",
markdown="# Hello",
)
except NotionifyRateLimitError as e:
print(f"Rate limited, retry after: {e.context}")
except NotionifyError as e:
print(f"[{e.code}] {e.message}")
Documentation
- Quickstart — Installation, first page, async usage
- API Reference — Full method signatures, config, result types
- Conversion Matrix — Markdown/Notion compatibility table
- Error Cookbook — Handling rate limits, images, conflicts
- Migration Guide — Upgrading from v1.x/v2.x
- FAQ — Common questions and answers
Development
# Clone and install dev dependencies
git clone https://github.com/notionify/notionify.git
cd notionify
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Lint and type-check
ruff check src/ tests/
mypy src/notionify # strict mode via pyproject.toml
# Performance benchmarks
pytest tests/perf/ -v -s
License
Metadata
Release files for notionify 0.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| notionify-0.2.3.tar.gz | 438.0 kB | With provenance | Details for notionify-0.2.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| notionify-0.2.3-py3-none-any.whl | Python 3 | none | any | Details for notionify-0.2.3-py3-none-any.whl |
Total release size: 551.1 kB
Release files / notionify-0.2.3.tar.gz
| Download URL | notionify-0.2.3.tar.gz |
|---|---|
| Size | 438.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ff535b6088d6227bcdb89754c60b4a72cbbaf37951410578d07783fd3fbcb7b2
|
|
BLAKE2b-256 checksum How to use checksums |
ee282f96bd9f9580bac610d27fef66d13d50f55db1c8ffbef4e4cf76cb7049d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 2, 2026.
Transparency logRelease files / notionify-0.2.3-py3-none-any.whl
| Download URL | notionify-0.2.3-py3-none-any.whl |
|---|---|
| Size | 113.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c6a603548a4e8b0af7f75b4f9c80375b52a2f71cfa906ff0519cbe7edcd0066f
|
|
BLAKE2b-256 checksum How to use checksums |
4c3886b246abf7d0314c514468d2caf8f7780327a8a12a9e72254bae0054a1e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 2, 2026.
Transparency logSecurity for notionify 0.2.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for notionify-0.2.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.