A production-ready Go CLI tool that converts Markdown to Notion blocks and uploads them to Notion pages via the public REST API.
- Zero dependencies: Direct HTTP calls to Notion API, no external SDK required
- CommonMark + GitHub flavored Markdown: Support for headings, paragraphs, lists, code blocks, quotes, images, and more
- Flexible operations: Append to existing pages, replace content, or create new pages
- Robust error handling: Rate limiting, retry logic with exponential backoff, and clear error messages
- Developer-friendly: Dry-run mode, verbose logging, stdin support
go install github.com/wiremind/markdown-to-notionapi/cmd/notion-md@latest
-
Create a Notion Integration:
- Go to https://www.notion.so/my-integrations
- Click "New integration" and give it a name
- Copy the "Internal Integration Token"
-
Share your page with the integration:
- Open your Notion page
- Click "Share" → "Invite" → Select your integration
-
Set your token:
export NOTION_TOKEN="secret_abc123..."
-
Upload Markdown:
md2notion --page-id YOUR_PAGE_ID --md document.md
md2notion --page-id abc123def456 --md notes.md
cat README.md | md2notion --page-id abc123def456
md2notion --page-id abc123def456 --md document.md --replace
md2notion --create --parent-id xyz789 --title "My Document" --md notes.md
md2notion --page-id abc123def456 --md notes.md --dry-run
md2notion --page-id abc123def456 --md notes.md --image-base-url "https://example.com/assets/"
Markdown | Notion Block |
---|---|
# ## ### |
heading_1/2/3 |
Paragraphs | paragraph |
bold, italic, code |
Rich text formatting |
Rich text formatting | |
links | Rich text links |
- bulleted lists |
bulleted_list_item |
1. numbered lists |
numbered_list_item |
Nested lists | Nested list items |
> blockquotes |
quote |
```code blocks``` |
code |
--- horizontal rules |
divider |
 |
image (external URLs only) |
Usage: md2notion [options]
Options:
--page-id string Notion page ID to append blocks to (required unless --create is set)
--parent-id string Parent page ID for creating new page (required if --create is set)
--title string Title for new page (required if --create is set)
--md string Path to markdown file (default: read from stdin)
--append Append blocks to the end of the page (default true)
--replace Replace existing page content
--create Create a new page
--image-base-url string Base URL for relative image paths
--dry-run Print JSON that would be sent, don't call API
--notion-version string Notion API version (default "2022-06-28")
-v, --verbose Verbose output
--timeout duration HTTP request timeout (default 1000s)
-h, --help Show help
Environment Variables:
NOTION_TOKEN Notion integration token (required)
If your page URL is https://notion.so/My-Page-abc123def456
, the page ID is abc123def456
.
You can also use the Notion API to list pages in a database or workspace.
The tool provides clear, actionable error messages:
- Authentication errors: Check your
NOTION_TOKEN
- Permission errors: Ensure the integration has access to the page
- Rate limiting: Automatically handled with exponential backoff
- Invalid page ID: Verify the page ID format and permissions
- Images: Only external URLs are supported (no binary upload)
- Tables: Not supported (would require complex Notion table structure)
- Advanced formatting: Some complex Markdown features may not translate perfectly
git clone https://github.com/wiremind/markdown-to-notionapi.git
cd markdown-to-notionapi
make dev-setup # Install development tools
make build # Build the binary
make help # Show all available commands
make test # Run tests
make coverage # Run tests with coverage report
make lint # Run golangci-lint
make security # Run security scans
make check # Run all quality checks
make build # Build binary
make build-all # Build for all platforms
make container-build # Build container image
make clean # Clean build artifacts
# Unit tests
make test
# With coverage
make coverage
# All quality checks
make check
# Set up test environment
export NOTION_TOKEN="your-integration-token"
export TEST_PAGE_ID="your-test-page-id"
# Test locally
make run-local
# Test container
make container-test
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '1.25'
- run: go test ./...
- run: go vet ./...
- run: go build ./cmd/notion-md
# Pull the latest image
docker pull ghcr.io/wiremind/markdown-to-notionapi:latest
# Run with a file
docker run --rm -v $(pwd):/data -e NOTION_TOKEN=$NOTION_TOKEN \
ghcr.io/wiremind/markdown-to-notionapi:latest \
-page-id=YOUR_PAGE_ID -md=/data/document.md
# Run with stdin
cat README.md | docker run --rm -i -e NOTION_TOKEN=$NOTION_TOKEN \
ghcr.io/wiremind/markdown-to-notionapi:latest \
-page-id=YOUR_PAGE_ID
# Using Docker
docker build -f Containerfile -t md2notion .
# Using Podman (also works with Containerfile)
podman build -f Containerfile -t md2notion .
# Run the container
docker run --rm -e NOTION_TOKEN=$NOTION_TOKEN md2notion -page-id=YOUR_PAGE_ID -dry-run
name: Upload to Notion
on: [push]
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Upload README to Notion
uses: docker://ghcr.io/wiremind/markdown-to-notionapi:latest
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
with:
args: -page-id=${{ secrets.NOTION_PAGE_ID }} -verbose README.md
name: Upload to Notion
on: [push]
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Upload documentation
uses: wiremind/markdown-to-notionapi@main
with:
page-id: ${{ secrets.NOTION_PAGE_ID }}
markdown-file: 'docs/api.md'
dry-run: 'false'
verbose: 'true'
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
- name: Upload inline content
uses: wiremind/markdown-to-notionapi@main
with:
page-id: ${{ secrets.NOTION_PAGE_ID }}
markdown-content: |
# Build Report
**Status**: ✅ Success
**Commit**: ${{ github.sha }}
**Branch**: ${{ github.ref_name }}
**Time**: ${{ github.event.head_commit.timestamp }}
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
Input | Description | Required | Default |
---|---|---|---|
page-id |
Notion page ID to upload to | ✅ | |
markdown-file |
Path to markdown file | ||
markdown-content |
Inline markdown content | ||
notion-token |
Notion token (or use env var) | ||
dry-run |
Only show what would be uploaded | false |
|
verbose |
Enable verbose output | false |
|
replace |
Replace page content instead of append | false |
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
MIT License - see LICENSE file for details.
Set your integration token:
export NOTION_TOKEN="secret_your_token_here"
- Verify the page ID is correct
- Ensure the integration has access to the page (share the page with your integration)
- Check that the token is valid and not expired
- Only external URLs (http/https) are supported
- Use
--image-base-url
for relative paths - Verify image URLs are publicly accessible
The tool automatically handles rate limiting with exponential backoff. If you hit limits frequently, consider:
- Reducing concurrency
- Adding delays between operations
- Processing smaller batches
For very large Markdown files:
- The tool automatically chunks requests (100 blocks max per API call)
- Use
--verbose
to monitor progress - Consider breaking large documents into smaller sections