Skip to content

Copy-paste examples for the HTML to PDF API in cURL, JavaScript, PHP and Python. Each one reads page.html, writes page.pdf, and fails loudly when the API answers with an error and not a file. I ran all of them before putting them here.

They all send layout=document, which renders the HTML the way you wrote it, over as many pages as it needs. Without it you get the older single_page default: one A4 page, everything in Arial, and whatever doesn't fit cut off. The overview guide has the rendering rules.

This is the page.html they use:

<!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>

cURL

The trick with curl is getting a whole HTML file into one parameter without fighting the shell over quotes. --data-urlencode with name@file does it:

curl -X POST "https://apixies.io/api/v1/html-to-pdf" \
     -H "X-API-Key: YOUR_API_KEY" \
     --data-urlencode "html@page.html" \
     -d "layout=document" \
     --max-time 60 \
     -o page.pdf

That reads page.html, URL-encodes it and sends it as the html field, with layout as a second form field. No escaping by hand.

If you'd rather send JSON, let jq build the body. -Rs reads the file as one raw string:

jq -Rs '{html: ., layout: "document"}' page.html | curl -X POST "https://apixies.io/api/v1/html-to-pdf" \
     -H "X-API-Key: YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     --data-binary @- \
     -o page.pdf

Don't build that JSON with echo and string concatenation. The first double quote in your HTML breaks it.

Catch errors

Here's the trap. -o page.pdf saves whatever comes back. With a wrong key, this is what ends up in your "PDF":

{"status":"error","http_code":401,"code":"INVALID_API_KEY","message":"Invalid API key.","errors":{"X-API-Key":["The API key is invalid or has been revoked."]}}

curl exits with 0, because the transfer worked. So check the status yourself:

#!/bin/bash
# topdf.sh page.html page.pdf
STATUS=$(curl -s -X POST "https://apixies.io/api/v1/html-to-pdf" \
  -H "X-API-Key: $APIXIES_API_KEY" \
  --data-urlencode "html@$1" \
  -d "layout=document" \
  --max-time 60 \
  -o "$2" -w "%{http_code}")

if [ "$STATUS" != "200" ]; then
  echo "Failed with HTTP $STATUS:" >&2
  cat "$2" >&2; echo >&2
  rm -f "$2"
  exit 1
fi

Now a failure prints the JSON, deletes the fake PDF and returns exit code 1, which is what cron and CI need.

--max-time 60 matters too. Rendering starts a real browser, so allow a few seconds. Without a limit, a stuck connection hangs your script forever.

JavaScript

Node 18 or newer, no packages:

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")));

Use arrayBuffer(), not text(). Reading a PDF as text mangles the bytes and you get a file that won't open.

To hand the PDF to a browser as a download, send the buffer with the right headers. With plain node:http:

import http from "node:http";

http.createServer(async (req, res) => {
  try {
    const pdf = await htmlToPdf(renderReport(req.url));
    res.writeHead(200, {
      "Content-Type": "application/pdf",
      "Content-Disposition": 'attachment; filename="report.pdf"',
    });
    res.end(pdf);
  } catch (error) {
    res.writeHead(502, { "Content-Type": "text/plain" });
    res.end("Couldn't build the PDF. Try again in a moment.");
  }
}).listen(3000);

renderReport() is yours. It returns the HTML string. In Express the body of the handler is the same, with res.set() and res.send(pdf).

Keep the call on your server. From browser JavaScript you'd have to ship your API key to every visitor.

PHP

function htmlToPdf(string $html): string
{
    $ch = curl_init('https://apixies.io/api/v1/html-to-pdf');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode(['html' => $html, 'layout' => 'document']),
        CURLOPT_HTTPHEADER => [
            'X-API-Key: ' . getenv('APIXIES_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 60,
    ]);

    $body = curl_exec($ch);
    $type = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException('Request failed');
    }

    if (! str_starts_with($type, 'application/pdf')) {
        $error = json_decode($body, true);
        throw new RuntimeException(($error['code'] ?? 'ERROR') . ': ' . ($error['message'] ?? 'Unknown error'));
    }

    return $body;
}

file_put_contents('page.pdf', htmlToPdf(file_get_contents('page.html')));

In Laravel the HTTP client makes it shorter, and a Blade view gives you the HTML:

$response = Http::withHeaders(['X-API-Key' => config('services.apixies.key')])
    ->timeout(60)
    ->post('https://apixies.io/api/v1/html-to-pdf', [
        'html' => view('pdf.report', $data)->render(),
        'layout' => 'document',
    ]);

abort_unless(str_starts_with((string) $response->header('Content-Type'), 'application/pdf'), 502);

return response($response->body(), 200, [
    'Content-Type' => 'application/pdf',
    'Content-Disposition' => 'attachment; filename="report.pdf"',
]);

Python

import os
import requests

def html_to_pdf(html):
    res = requests.post(
        "https://apixies.io/api/v1/html-to-pdf",
        json={"html": html, "layout": "document"},
        headers={"X-API-Key": os.environ["APIXIES_API_KEY"]},
        timeout=60,
    )
    if not res.headers.get("Content-Type", "").startswith("application/pdf"):
        body = res.json()
        raise RuntimeError(f"{body['code']}: {body['message']}")
    return res.content

with open("page.html", encoding="utf-8") as f:
    pdf = html_to_pdf(f.read())

with open("page.pdf", "wb") as f:
    f.write(pdf)

res.content is bytes, res.text isn't. And open the output file with "wb".

Errors you'll meet

All four examples turn these into an exception or a non-zero exit:

Status Code What to do
401 MISSING_AUTH, INVALID_API_KEY Check the X-API-Key header
422 VALIDATION_FAILED html is missing or bigger than 512 KB, or layout has a value it doesn't know
429 DAILY_QUOTA_EXCEEDED The free tier is 75 requests a day. Wait for the reset, and store PDFs you've already made
503 PDF_GENERATION_FAILED The renderer didn't start or timed out. Retry once after a short pause

A typo in the layout is one of those 422s, and the errors object tells you which field it was:

{"status":"error","http_code":422,"code":"VALIDATION_FAILED","message":"Validation failed.","errors":{"layout":["The selected layout is invalid."]}}

One thing isn't an error and probably should be. Leave layout out completely and the request succeeds in single_page mode, where content longer than one A4 page is cut off and you still get a 200. If a PDF comes back short, check that the field made it into the request.

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