Skip to content

Send HTML, get a PDF back. One POST request, no Chromium to install, no Docker image to babysit. This page covers how the HTML to PDF API behaves, including the parts that will surprise you if nobody tells you first.

The request

curl -X POST "https://apixies.io/api/v1/html-to-pdf" \
     -H "X-API-Key: YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"layout":"document","html":"<!DOCTYPE html><html><body><h1>Hello PDF</h1></body></html>"}' \
     -o hello.pdf

That writes an A4 PDF to hello.pdf. There are two parameters:

  • html (required): the document as a string, up to 512 KB.
  • layout: send document. It renders your HTML the way you wrote it, over as many pages as it needs.

Leave layout out and you get the older default, single_page. That one squeezes everything onto one A4 page, sets all text in Arial, and cuts off whatever doesn't fit, without an error. It's still the default so that integrations built on it keep their output. If a PDF comes back with the bottom half missing, that's what happened. Everything on this page assumes document.

You can send the body as JSON or as normal form fields. Both work.

The response isn't JSON. It's the file itself:

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: inline; filename="document.pdf"

Errors are JSON, in the same envelope as every other Apixies endpoint. Leave out html and you get:

{
  "status": "error",
  "http_code": 422,
  "code": "VALIDATION_FAILED",
  "message": "Validation failed.",
  "errors": { "html": ["The html field is required."] }
}

So check the Content-Type before you save anything. If you don't, a bad key gives you a file called invoice.pdf with an error message inside, and you find out when a customer opens it.

Status Code Why
401 MISSING_AUTH, INVALID_API_KEY No key, or a wrong one
422 VALIDATION_FAILED html is missing or over 512 KB, or layout isn't document or single_page
429 DAILY_QUOTA_EXCEEDED You've used today's requests
503 PDF_GENERATION_FAILED The renderer didn't start or timed out. Retry

Rendering runs a real browser, so allow a few seconds and set a timeout on your side. 60 seconds is plenty.

What your HTML can and can't do

I rendered a pile of test documents with layout=document to find the edges. This is what came out of it.

Long documents paginate. I sent 120 short paragraphs and got five pages, with the last line on the last page. The usual print CSS works: page-break-before: always started a new page where I put it, and break-inside: avoid on a table row moved a tall row to the next page instead of slicing it in half.

The page is A4 with 10 mm margins, until you say otherwise. @page is respected. With @page { size: A5 landscape; margin: 30mm } I got an A5 landscape PDF. Margin on body is respected too, and it's added to the page margin, so set one of them to 0. For a page that bleeds to the edge, use @page { margin: 0 }.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    @page { size: A4; margin: 20mm; }
    body { margin: 0; font-family: Georgia, serif; }
    h1 { color: #0b4f8a; border-bottom: 2px solid #0b4f8a; padding-bottom: 8px; }
    p { line-height: 1.6; }
  </style>
</head>
<body>
  <h1>Monthly report</h1>
  <p>Revenue is up 12% on last month. Café sales: 1 240 €.</p>
</body>
</html>

Your font-family counts, but only installed fonts exist. I looked at the finished PDFs with pdffonts. On my machine Georgia came out as Liberation Serif, Arial as Liberation Sans and Courier New as Liberation Mono, which are the usual Linux stand-ins with the same letter widths. A family nobody has installed ("Brandon Grotesque") fell back to a serif, which is rarely what you want. So always end the list with sans-serif, serif or monospace. The production server may carry a different set of fonts than mine, so check one PDF before you settle on a look.

Nothing is fetched from the network. The browser renders your HTML offline. A remote <img> shows its alt text, a stylesheet on a CDN is ignored, a Google Font never arrives. CSS has to be in a <style> block or inline.

Images go in as data URIs. <img src="data:image/png;base64,..."> works. I also ran gif and svg+xml, and jpeg and webp are let through the same way. An inline <svg> element and a CSS background: url(data:...) work as well. Mind the 512 KB limit: base64 makes a file a third bigger, so this is for logos, not photos.

Page numbers work, through @page margin boxes. I didn't expect this one. Chromium fills them in, counters included:

@page {
  margin: 25mm;
  @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 10pt; }
}

Every page of my five-page test said "Page 2 of 5" and so on. A position: fixed element repeats on every page too, but it sits on top of the text from page two onward, because the space you left for it only exists on page one. Use the margin boxes.

A fragment is fine. <h1>Hello</h1> without a doctype gets a bare <html><body> around it and nothing else.

No JavaScript. <script> tags are removed before rendering, along with iframes and event handlers. If your page builds itself with JS, render it to HTML first.

Everything else is a current Chromium: flexbox, grid, tables, gradients, UTF-8 (the euro sign and the é above come through fine).

From code

The pattern is the same in any language. POST the HTML, check the content type, then write bytes. In Node:

import { readFile, writeFile } from "node:fs/promises";

async function htmlToPdf(html) {
  const res = await fetch("https://apixies.io/api/v1/html-to-pdf", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.APIXIES_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, layout: "document" }),
    signal: AbortSignal.timeout(60_000),
  });

  if (!res.headers.get("content-type")?.startsWith("application/pdf")) {
    const body = await res.json();
    throw new Error(`${body.code}: ${body.message}`);
  }
  return Buffer.from(await res.arrayBuffer());
}

await writeFile("page.pdf", await htmlToPdf(await readFile("page.html", "utf8")));

PHP, Python and the curl details are in the code examples guide.

Is it the right tool?

It fits documents you build from data: invoices, receipts, tickets, certificates, a report of a few pages. The invoice guide walks through one from template to file, including a table that runs over a page break.

It's the wrong tool for brand fonts, since remote fonts don't load, and for printing a live web page with its images and scripts. For those, run Puppeteer or Playwright yourself. You'll pay for it with a Chromium install and the memory it eats, but you get every option. For a picture of a live page, the Screenshot API does fetch from the network. And if your source is Markdown, the Markdown to PDF API saves you the HTML step.

The free tier is 75 requests a day, and each PDF is one request. So store the file once it's made. Don't render it again every time someone clicks download.

Next steps

Try the HTML to PDF Converter API

Free tier is for development & small projects. 75 requests/day with a registered account.

cookies

We use analytics cookies to see how the site gets used. Nothing loads until you accept. Privacy policy