PDF · DOCX · HTML · REST API · Docker

Your Markdown.
Beautiful Docs.

Drop your .md file, paste your text, or call the REST API from any app. Export pixel-perfect PDF, DOCX, or HTML — Mermaid diagrams, LaTeX math, YAML frontmatter, and zero overflow.

Three ways to convert

Editor, file upload, or URL — pick your workflow.

Markdown
Preview

Your preview will appear here

From text to PDF in three steps

01

Provide your Markdown

Type directly in the editor, drag-and-drop a .md file, or paste a raw URL from GitHub, GitLab, or any public source.

02

Tune the options

Toggle a Table of Contents, enable automatic page breaks at every chapter, or set a custom document title shown in the footer.

03

Download your document

Choose PDF, DOCX, or HTML. Pixel-perfect output with no layout hacks, no overflow — ready to share or publish.

Everything your docs need

Cover Page, Headers & Footers

Auto-generated title page with title, author, and date. Document title in the header, page X / Y in the footer — cover page gets blank headers automatically.

Table of Contents & Numbered Sections

Clickable TOC with correct nesting. Optional 1.1.1 heading numbering for formal documents. In DOCX, a native Word TOC field.

Syntax Highlighting

190+ languages with GitHub-light theme. Code blocks with gray backgrounds, borders, and monospace fonts — never overflows the page.

Smart Tables

Pipe, grid, and multiline tables. Header shading, alternating row colors, borders — auto-scaled to page width so nothing overflows.

Mermaid Diagrams

Flowcharts, sequence diagrams, git graphs, Gantt charts, ER diagrams, pie charts, mindmaps — rendered as crisp SVGs in both PDF and DOCX.

Math, Footnotes & Lists

LaTeX math via KaTeX, numbered footnotes, task lists, definition lists, and full GitHub Flavoured Markdown — strikethrough, autolinks, and more.

Images & Figure Captions

Local images base64-inlined so they always render. Alt text becomes figure captions in DOCX. Constrained to page width automatically.

Page Break Control

Manual <!-- pagebreak --> or auto-break before every H1. TOC gets its own page. Full control over document flow.

HTML Export

Self-contained HTML with all images base64-inlined. Includes TOC, syntax highlighting, and full styling — ready to open in any browser.

YAML Frontmatter & Variables

Add a YAML block at the top of any .md file to set title, author, page size, watermark, theme, and more. Use {{title}} and {{author}} placeholders anywhere in the document.

Watermarks & Page Size

Add a diagonal watermark (DRAFT, CONFIDENTIAL, etc.) to every page. Choose from A4, Letter, A3, A5, or Legal — portrait or landscape.

Async Jobs & Webhooks

Enqueue large conversions via POST /api/v1/jobs, poll with GET /api/v1/jobs/:id, and get notified via webhook when done. Prometheus metrics at /api/v1/metrics.

Integrate InkDown anywhere

Call the conversion API from any language. Build pipelines, automate doc generation, or embed into your own platform.

Base URL http://localhost:3000/api/v1
Auth Optional. Set INKDOWN_API_KEYS env var to require an API key. Pass it as X-API-Key: <key> or Authorization: Bearer <key>.
Formats application/json multipart/form-data
Endpoints
GET /api/v1/health Server health check — returns status and version
GET /api/v1/info API metadata — formats, auth mode, all endpoints
POST /api/v1/convert Convert Markdown → PDF, DOCX, or HTML
POST /api/v1/merge Merge multiple Markdown documents into one output
POST /api/v1/jobs Enqueue an async conversion job — returns a job ID immediately
GET /api/v1/jobs/:id Poll async job status; add ?download=1 to fetch the result file
GET /api/v1/metrics Prometheus-compatible metrics (uptime, conversions, job stats)
POST /api/v1/convert
Parameter Type Required Description
markdown string required* Raw Markdown content to convert
url string required* Public URL of a raw .md file to fetch and convert
file file required* Multipart file upload of a .md file
format string optional "pdf" (default), "docx", or "html"
title string optional Document title shown in the PDF footer. Defaults to filename.
author string optional Author name shown on cover / metadata.
toc boolean optional Generate a Table of Contents from headings. Default: false
autoBreak boolean optional Insert a page break before every H1 heading. Default: false
pageSize string optional "A4" (default), "Letter", "A3", "A5", or "Legal"
landscape boolean optional Landscape orientation (PDF only). Default: false
watermark string optional Diagonal watermark text on every page, e.g. "DRAFT" or "CONFIDENTIAL"

* Exactly one of markdown, url, or a multipart file is required.

Code Examples
curl -X POST http://localhost:3000/api/v1/convert \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -d '{
    "markdown": "# Hello World\n\nThis is my document.",
    "format": "pdf",
    "title": "My Document",
    "author": "Jane Smith",
    "pageSize": "Letter",
    "toc": true,
    "watermark": "DRAFT"
  }' \
  --output document.pdf
const response = await fetch('http://localhost:3000/api/v1/convert', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your_api_key',
  },
  body: JSON.stringify({
    markdown: '# Hello World\n\nThis is my document.',
    format: 'pdf',   // 'pdf' | 'docx' | 'html'
    title: 'My Document',
    author: 'Jane Smith',
    pageSize: 'Letter',
    toc: true,
    watermark: 'DRAFT',
  }),
});

if (!response.ok) {
  const err = await response.json();
  throw new Error(err.error);
}

// Save as file (browser)
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.pdf';
a.click();
import requests

response = requests.post(
    'http://localhost:3000/api/v1/convert',
    headers={'X-API-Key': 'your_api_key'},
    json={
        'markdown': '# Hello World\n\nThis is my document.',
        'format': 'html',   # 'pdf' | 'docx' | 'html'
        'title': 'My Document',
        'author': 'Jane Smith',
        'toc': True,
    },
)
response.raise_for_status()

with open('document.html', 'wb') as f:
    f.write(response.content)

print('Saved document.html')
<?php
$ch = curl_init('http://localhost:3000/api/v1/convert');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-API-Key: your_api_key',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'markdown'  => "# Hello World\n\nThis is my document.",
        'format'    => 'pdf',
        'title'     => 'My Document',
        'toc'       => true,
    ]),
]);

$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status === 200) {
    file_put_contents('document.pdf', $pdf);
    echo "Saved document.pdf\n";
}
import fs from 'fs';
import FormData from 'form-data';
import fetch from 'node-fetch';

// Option A — send markdown as JSON
const res = await fetch('http://localhost:3000/api/v1/convert', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your_api_key',
  },
  body: JSON.stringify({
    markdown: fs.readFileSync('README.md', 'utf-8'),
    format: 'pdf',
    title: 'README',
  }),
});

// Option B — send as file upload
const form = new FormData();
form.append('file', fs.createReadStream('README.md'));
form.append('format', 'pdf');
const res2 = await fetch('http://localhost:3000/api/v1/convert', {
  method: 'POST',
  headers: { 'X-API-Key': 'your_api_key', ...form.getHeaders() },
  body: form,
});

fs.writeFileSync('output.pdf', Buffer.from(await res.arrayBuffer()));
Responses
200 OK Binary file stream with Content-Disposition: attachment; filename="..."
400 { "error": "...", "code": "BAD_REQUEST" } — missing input or invalid URL
401 { "error": "...", "code": "UNAUTHORIZED" } — invalid or missing API key
500 { "error": "...", "code": "CONVERSION_ERROR" } — conversion failed
GET /api/v1/health — Example Response
{
  "status": "ok",
  "version": "1.0.0",
  "timestamp": "2026-06-06T10:00:00.000Z",
  "formats": ["pdf", "docx", "html", "epub", "slides"]
}

Run InkDown as a container

Pre-built image on Docker Hub. No Node.js, no Chrome, no Pandoc install — everything is bundled. Works on Linux, macOS, and Windows.

Terminal
$ docker run -p 3000:3000 aryansin1234/inkdown:latest
InkDown
─────────────────────────────────────────────
Web App : http://localhost:3000
API v1 : http://localhost:3000/api/v1
Auth : open — set INKDOWN_API_KEYS to restrict
Runs on Linux macOS Windows · Image Docker Hub
01
Pull & Run Fastest way to get started
one-liner
$ docker run -p 3000:3000 aryansin1234/inkdown:latest
docker run -p 3000:3000 aryansin1234/inkdown:latest
  • Web UI at localhost:3000
  • REST API at /api/v1/convert
  • No auth required by default
02
Docker Compose Persistent, configurable setup
recommended
$ curl -O https://raw.githubusercontent.com/Aryansin1234/InkDown/docker-version/docker-compose.yml
$ docker compose up
curl -O https://raw.githubusercontent.com/Aryansin1234/InkDown/docker-version/docker-compose.yml
docker compose up
  • Auto-restart on reboot
  • Edit the file to set env vars
  • Built-in health check
03
Secured with API Key Production-grade access control
secure
$ docker run -p 3000:3000 \
  -e
INKDOWN_API_KEYS=my-secret-key \
  
aryansin1234/inkdown:latest
docker run -p 3000:3000 \
  -e INKDOWN_API_KEYS=my-secret-key \
  aryansin1234/inkdown:latest
  • Pass as X-API-Key: ...
  • Or Authorization: Bearer ...
  • Multiple keys, comma-separated
What’s inside the image
node:20-slim Chromium (system) Pandoc (DOCX) Puppeteer (no-download) marked · highlight.js Non-root user Express · multer · cors ~400 MB
Environment Variables
Variable Default Description
PORT 3000 HTTP port the server listens on inside the container
INKDOWN_API_KEYS unset · open Comma-separated API keys. When set, /api/v1/convert requires a valid key.
INKDOWN_CORS_ORIGINS * Restrict CORS. E.g. https://myapp.com,https://staging.myapp.com
PUPPETEER_EXECUTABLE_PATH /usr/bin/chromium Chromium binary path — pre-configured in the image, no need to change
Build from Source
1
Clone the repository
$ git clone https://github.com/Aryansin1234/InkDown.git && cd InkDown
2
Build the Docker image
$ docker build -t aryansin1234/inkdown .
3
Run locally
$ docker run -p 3000:3000 aryansin1234/inkdown
git clone https://github.com/Aryansin1234/InkDown.git && cd InkDown
docker build -t aryansin1234/inkdown .
docker run -p 3000:3000 aryansin1234/inkdown