The one-line difference
WeasyPrint is a Python library that converts HTML + CSS to PDF using its own layout engine (no Chromium). makesPDF is a hosted REST API: any language can POST /api/v1/md and get a deterministic, tagged, archival PDF back. No Pango, no Cairo, no GTK headers, no apt install list, no Python version drift, no CI image to maintain.
Feature matrix
| WeasyPrint | makesPDF | |
|---|---|---|
| Shape | Python library | Hosted REST API (/api/v1/*) |
| Authoring surface | HTML + CSS | Markdown, builder DSL, JSON |
| Runtime | Your Python process + native deps (Pango, Cairo, harfbuzz) | Cloudflare Workers (we host) |
| Install footprint | Python + ~5 native libraries | 0 — you call an HTTP endpoint |
| Tagged PDF (PDF/UA-1) | Partial — emits structure but coverage gaps remain | Yes, full structure tree, by default |
| PDF/A archival | PDF/A-1, -2, -3 | PDF/A-2A |
| veraPDF-validated | Mixed (depends on input HTML) | Yes |
| Markdown input | No (HTML only) | Yes (POST /api/v1/md) |
| Determinism | Subject to Pango/font drift | Byte-stable across deploys |
| Agent-first | No | Yes — public skill file, MCP server, x402-payable |
| License | BSD-3-Clause | Proprietary hosted; skill file MIT |
Same input, two outputs
A "Hello, Ada" report.
WeasyPrint — Python + HTML + CSS:
from weasyprint import HTML, CSS
html = """
<!doctype html>
<html><body>
<h1>Hello, Ada</h1>
<p>Welcome to the report.</p>
</body></html>
"""
css = CSS(string="""
@page { size: A4; margin: 40pt; }
body { font-family: sans-serif; font-size: 12pt; }
h1 { font-size: 24pt; margin-bottom: 12pt; }
""")
HTML(string=html).write_pdf("hello.pdf", stylesheets=[css])
Plus the install dance: apt install libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libcairo2, pin a compatible WeasyPrint version, hope your CI image still has the right glibc.
makesPDF — same Python, one HTTP call, no native deps:
import requests, os
resp = requests.post(
"https://makespdf.com/api/v1/md",
headers={"Authorization": f"Bearer {os.environ['MAKESPDF_API_KEY']}"},
json={"markdown": "# Hello, Ada\n\nWelcome to the report."},
)
resp.raise_for_status()
open("hello.pdf", "wb").write(resp.content)
Why people switch
- The dependency footprint. WeasyPrint pulls Pango, Cairo, harfbuzz, fontconfig, and a stack of font packages into your image. CI builds slow down, base images grow, and a glibc bump in your Docker base can break PDF output. Hosted means none of that lives in your repo.
- Markdown is faster than HTML for content. Most documents are mostly content. Writing markdown — or generating it from a database — is a fraction of the work of templating HTML + a CSS print stylesheet.
- Compliance is structural. WeasyPrint emits a structure tree but coverage is uneven, and validating PDF/UA-1 against veraPDF often needs hand-tuning per template. makesPDF tags by construction and validates by default.
- Determinism. Pango shaping and font-fallback behaviour change between versions. Same HTML can produce slightly different line breaks across deploys. Our renderer is a single owned pipeline; output is byte-stable.
- Agents. A Python wrapper is fine for batch jobs; it's awkward for an agent. A REST endpoint with a skill file is the agent-native path.
When WeasyPrint is still the right call
- Air-gapped or strictly-on-prem. A self-hosted Python library with no egress is the obvious answer.
- You already have a CSS-print stylesheet that's been tuned for years and you don't want to migrate.
- BSD-licensed, fork-and-audit is a hard procurement requirement.
- Genuinely complex CSS layouts (CSS Grid templates, advanced page rules) we don't support.
For most Python teams who just want a PDF endpoint that works the same way in dev, CI, and prod, the hosted API removes a whole class of native-dependency problems.
Migration
Most WeasyPrint code paths are converting a Jinja-templated HTML string. Two paths:
- Markdown — render your data into markdown and POST to
/api/v1/md. Headings, tables, alerts, footnotes, and Mermaid diagrams all render natively. - DSL — for tables-heavy documents (invoices, receipts, statements), the builder DSL gives you precise grid + colspan control without a CSS-print stylesheet.
Both endpoints accept JSON, so the Python migration is a requests.post call.