Skip to content
tutorial

Generate PDFs from HTML and Markdown with a REST API

| 6 min read
Printed document pages on a desk
Photo by Bank Phrom on Unsplash

Your app generates invoices. You have the HTML template. Now you need to turn it into a downloadable PDF. The standard approach: install Puppeteer, launch a headless Chromium instance, call page.pdf(). That's 300-500 MB of dependencies for a print function.

It gets worse in production. Chromium leaks memory under load. Docker images bloat past 1 GB. Cold starts take 3-8 seconds. You end up maintaining a browser process pool for something that should be a single function call.

There's a shorter path. Send your HTML to an API, get a PDF back.

HTML to PDF in one POST request

The /v1/pdf/from-html endpoint accepts an HTML string and returns a PDF. No browser binary, no dependencies, no configuration.

curl -X POST https://api.botoi.com/v1/pdf/from-html \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Invoice #1042</h1><p>Amount due: $2,450.00</p>"
  }'

Response:

{
  "success": true,
  "data": {
    "url": "https://api.botoi.com/pdfs/f7a2c1e8.pdf",
    "size_bytes": 24576,
    "pages": 1
  }
}

The url field points to the generated PDF. Download it, redirect your user to it, or store it in your own S3 bucket. The API handles rendering, pagination, and font embedding.

Printed invoice document on a desk
Photo by Dimitri Karastelev on Unsplash

Markdown to PDF in one POST request

Not every document starts as HTML. Reports, changelogs, and documentation often live as Markdown. The /v1/pdf/from-markdown endpoint converts Markdown to a styled PDF directly.

curl -X POST https://api.botoi.com/v1/pdf/from-markdown \
  -H "Content-Type: application/json" \
  -d '{
    "markdown": "# Invoice #1042\n\n**Client:** Northwind Traders\n\n| Item | Qty | Price |\n|------|-----|-------|\n| SSL certificate audit | 1 | $450.00 |\n| Monthly API access | 3 | $57.00 |\n\n**Total: $621.00**"
  }'

Response:

{
  "success": true,
  "data": {
    "url": "https://api.botoi.com/pdfs/b3d9e4f1.pdf",
    "size_bytes": 21504,
    "pages": 1
  }
}

Tables, headings, bold text, code blocks, and lists all render correctly. The API applies clean typographic defaults so the output looks presentable without custom CSS.

Practical example: invoice generation

A realistic invoice needs a company header, billing address, line items table, tax calculation, and payment terms. Here's a full invoice sent as one API call:

curl -X POST https://api.botoi.com/v1/pdf/from-html \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-api-key" \
  -d '{
    "html": "<html><head><style>body { font-family: Helvetica, sans-serif; color: #1a1a1a; padding: 40px; } .header { display: flex; justify-content: space-between; margin-bottom: 40px; } .company { font-size: 24px; font-weight: bold; } .invoice-meta { text-align: right; color: #666; } table { width: 100%; border-collapse: collapse; margin: 20px 0; } th { background: #f5f5f5; text-align: left; padding: 12px; border-bottom: 2px solid #ddd; } td { padding: 12px; border-bottom: 1px solid #eee; } .total-row td { font-weight: bold; border-top: 2px solid #1a1a1a; } .footer { margin-top: 40px; font-size: 12px; color: #999; }</style></head><body><div class=\"header\"><div><div class=\"company\">Cascade Software LLC</div><div>742 Evergreen Terrace, Suite 200<br>Portland, OR 97201<br>billing@cascadesoftware.io</div></div><div class=\"invoice-meta\"><div style=\"font-size:28px;font-weight:bold\">INVOICE</div><div>#INV-1042</div><div>Date: March 15, 2026</div><div>Due: April 14, 2026</div></div></div><div><strong>Bill To:</strong><br>Northwind Traders Inc.<br>Attn: Jamie Chen<br>1200 Market Street<br>San Francisco, CA 94103</div><table><thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Amount</th></tr></thead><tbody><tr><td>API integration consulting (8 hrs)</td><td>8</td><td>$175.00</td><td>$1,400.00</td></tr><tr><td>SSL certificate audit and renewal</td><td>1</td><td>$450.00</td><td>$450.00</td></tr><tr><td>Monthly API access (Starter plan)</td><td>3</td><td>$9.00</td><td>$27.00</td></tr><tr><td>DNS security monitoring setup</td><td>1</td><td>$350.00</td><td>$350.00</td></tr></tbody><tbody><tr><td colspan=\"3\" style=\"text-align:right\">Subtotal</td><td>$2,227.00</td></tr><tr><td colspan=\"3\" style=\"text-align:right\">Tax (8.5%)</td><td>$189.30</td></tr><tr class=\"total-row\"><td colspan=\"3\" style=\"text-align:right\">Total Due</td><td>$2,416.30</td></tr></tbody></table><div class=\"footer\">Payment terms: Net 30. Please remit to Cascade Software LLC. Thank you for your business.</div></body></html>"
  }'

That single request produces a professional invoice PDF with Cascade Software's letterhead, four line items totaling $2,227.00, 8.5% sales tax of $189.30, and a grand total of $2,416.30. The CSS in the <style> tag controls fonts, spacing, table borders, and the bold total row. You can use any CSS the browser supports, including flexbox, grid, and @page rules for margins and page size.

Practical example: quarterly report from Markdown

Reports are a natural fit for Markdown. Product managers, analysts, and ops teams already write in Markdown. Converting to PDF for email distribution or archiving is one API call:

curl -X POST https://api.botoi.com/v1/pdf/from-markdown \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-api-key" \
  -d '{
    "markdown": "# Q1 2026 API Usage Report\n\n**Client:** Northwind Traders Inc.\n**Period:** January 1 - March 31, 2026\n**Generated:** March 29, 2026\n\n## Summary\n\n| Metric | Value |\n|--------|-------|\n| Total API calls | 284,391 |\n| Average daily calls | 3,160 |\n| Peak daily calls | 8,442 (Feb 14) |\n| Error rate | 0.3% |\n| P95 latency | 142ms |\n\n## Endpoint breakdown\n\n| Endpoint | Calls | Avg latency |\n|----------|-------|-------------|\n| /v1/email-mx/verify | 112,840 | 98ms |\n| /v1/ip/lookup | 89,221 | 34ms |\n| /v1/disposable-email/check | 52,106 | 22ms |\n| /v1/dns-security/spf-check | 18,440 | 156ms |\n| /v1/screenshot/capture | 11,784 | 1,240ms |\n\n## Recommendations\n\n1. Your email verification volume qualifies for the Pro plan at $49/mo (currently on Starter at $9/mo). This removes the 1,000 req/min cap.\n2. SPF check latency is elevated due to slow authoritative nameservers for 3 domains. Consider caching results for 1 hour.\n3. Screenshot captures account for 68% of your total latency budget. Batch these during off-peak hours if possible."
  }'

The API renders the tables, headings, bold text, and numbered list into a clean PDF. No intermediate HTML step on your side. Pass the Markdown string from your database, a Git repository, or a Notion export.

Practical example: receipt generation in Express

Here's an Express route that generates a PDF receipt on demand when a user requests one for a past order:

import express from "express";

const app = express();
app.use(express.json());

const BOTOI_API_KEY = process.env.BOTOI_API_KEY;

app.post("/receipts/:orderId", async (req, res) => {
  const { orderId } = req.params;

  // Fetch order from your database
  const order = await db.orders.findById(orderId);

  if (!order) {
    return res.status(404).json({ error: "Order not found" });
  }

  const receiptHtml = buildReceiptHtml(order);

  const pdfResponse = await fetch(
    "https://api.botoi.com/v1/pdf/from-html",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${BOTOI_API_KEY}`,
      },
      body: JSON.stringify({ html: receiptHtml }),
    }
  );

  const { data } = await pdfResponse.json();

  res.json({
    order_id: orderId,
    receipt_url: data.url,
    generated_at: new Date().toISOString(),
  });
});

function buildReceiptHtml(order) {
  const rows = order.items
    .map(
      (item) =>
        `<tr>
          <td>${item.name}</td>
          <td>${item.quantity}</td>
          <td>$${item.price.toFixed(2)}</td>
          <td>$${(item.quantity * item.price).toFixed(2)}</td>
        </tr>`
    )
    .join("");

  return `<html>
    <head>
      <style>
        body { font-family: Helvetica, sans-serif; padding: 40px; }
        h1 { font-size: 20px; }
        table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        th, td { padding: 8px; border-bottom: 1px solid #ddd; text-align: left; }
        .total { font-size: 18px; font-weight: bold; margin-top: 20px; }
      </style>
    </head>
    <body>
      <h1>Receipt #${order.id}</h1>
      <p>Date: ${new Date(order.created_at).toLocaleDateString()}</p>
      <p>Customer: ${order.customer_name}</p>
      <table>
        <thead><tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>
      <div class="total">Total: $${order.total.toFixed(2)}</div>
    </body>
  </html>`;
}

app.listen(3000);

The buildReceiptHtml function takes order data from your database and produces a styled HTML string. The route sends it to the botoi API and returns the PDF URL to the client. No Puppeteer. No file system operations. No temp directories.

Same pattern in Hono

If you're running on Cloudflare Workers or Bun, the Hono version is almost identical:

import { Hono } from "hono";

const app = new Hono();

app.post("/receipts/:orderId", async (c) => {
  const orderId = c.req.param("orderId");
  const order = await db.orders.findById(orderId);

  if (!order) {
    return c.json({ error: "Order not found" }, 404);
  }

  const html = buildReceiptHtml(order);

  const res = await fetch("https://api.botoi.com/v1/pdf/from-html", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${c.env.BOTOI_API_KEY}`,
    },
    body: JSON.stringify({ html }),
  });

  const { data } = await res.json();

  return c.json({
    order_id: orderId,
    receipt_url: data.url,
    generated_at: new Date().toISOString(),
  });
});

export default app;

The key difference: Hono accesses environment variables through c.env instead of process.env. Everything else stays the same. One fetch call, one JSON response, one PDF URL.

Comparison: API vs Puppeteer vs wkhtmltopdf vs Prince

Four common approaches to HTML-to-PDF conversion, compared on what matters in production:

Approach             | Setup time    | Memory       | Maintenance      | Cost (low vol)
─────────────────────|───────────────|──────────────|──────────────────|───────────────
botoi PDF API        | 0 min         | 0 MB         | None             | Free (5 req/min)
Puppeteer + page.pdf | 30-60 min     | 300-500 MB   | Chromium updates | Server cost
wkhtmltopdf          | 15-30 min     | 50-100 MB    | Binary patches   | Server cost
Prince XML           | 10 min        | 30 MB        | License renewal  | $3,800 license

Puppeteer gives you full browser control but carries heavy operational cost. You're running Chromium on your server, managing memory limits, and patching security vulnerabilities in the browser binary. Works well if you need JavaScript execution, cookie injection, or authenticated page rendering.

wkhtmltopdf is lighter than Puppeteer but uses the deprecated QtWebKit engine. CSS support is stuck at 2015 levels. Flexbox and grid don't work. The project is archived on GitHub with no active maintainers.

Prince XML produces the highest quality print output with full CSS Paged Media support. The tradeoff: a $3,800 license fee per server. That makes sense for publishing workflows, not for generating invoices in a SaaS app.

The API approach wins when you need PDFs from templates you control (invoices, receipts, reports, contracts) and don't need to render arbitrary authenticated pages. Zero setup, zero maintenance, predictable latency.

Key points

  • Two endpoints, two input formats. /v1/pdf/from-html for full HTML with CSS. /v1/pdf/from-markdown for Markdown content.
  • No server-side dependencies. No Puppeteer, no Chromium, no wkhtmltopdf binary. One HTTP call from any language.
  • Full CSS support. Inline styles, style tags, flexbox, grid, Google Fonts, and @page media queries all work.
  • Free tier available. 5 requests per minute with no API key. Paid plans start at $9/month for production workloads.
  • Common use cases. Invoices, receipts, reports, contracts, documentation exports, and email attachments.

Check the API docs for the full parameter reference, including page size, margins, and header/footer configuration.

Frequently asked questions

How do I generate a PDF from HTML programmatically?
Send a POST request to https://api.botoi.com/v1/pdf/from-html with a JSON body containing your HTML string. The API returns a base64-encoded PDF or a download URL. No browser binary, headless instance, or server-side dependency required.
Can I convert Markdown to PDF with this API?
Yes. Send a POST request to https://api.botoi.com/v1/pdf/from-markdown with your Markdown string. The API renders the Markdown to styled HTML internally and returns a PDF. Supports headings, tables, code blocks, and lists.
Does the API support CSS styling in the HTML input?
Yes. You can include inline styles, style tags, and full CSS in your HTML. The rendering engine supports flexbox, grid, custom fonts via Google Fonts links, and print-specific media queries like @page for margins and page size.
Is the PDF generation API free?
Anonymous access allows 5 requests per minute with IP-based rate limiting. No API key or signup required. Paid plans start at $9/month and remove the rate limit for production workloads like invoice generation or batch reporting.
How does the API compare to Puppeteer for PDF generation?
Puppeteer requires installing Chromium (300-500MB), managing browser instances, handling memory leaks, and maintaining version compatibility. The botoi API is a single HTTP call with zero infrastructure. Setup takes seconds instead of hours. For most PDF use cases (invoices, reports, receipts), the API covers what you need without the operational overhead.

Try this API

Markdown to HTML API — interactive playground and code examples

More tutorial posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.