html-to-pdf.eu

API documentation

One endpoint: POST https://html-to-pdf.eu/v1/pdf. Send JSON, get a PDF. Nothing is stored.

Authentication

Create an API key in your dashboard. Keys start with h2p_ and are shown once. Send the key in the header Authorization: Bearer h2p_.... Keys in the query string (?apiKey=) are rejected with 401.

Quick start

curl

curl -X POST https://html-to-pdf.eu/v1/pdf \
  -H "Authorization: Bearer $H2P_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Hello</h1><p>My first PDF.</p>", "fileName": "hello"}' \
  -o hello.pdf

C# (HttpClient)

using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new Uri("https://html-to-pdf.eu") };
http.DefaultRequestHeaders.Authorization =
    new("Bearer", Environment.GetEnvironmentVariable("H2P_KEY"));

var response = await http.PostAsJsonAsync("/v1/pdf", new
{
    html = "<h1>Hello</h1><p>My first PDF.</p>",
    format = "A4",
});
response.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync("hello.pdf", await response.Content.ReadAsByteArrayAsync());
Console.WriteLine(response.Headers.GetValues("X-Credits-Remaining").First());

Node (fetch)

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

const response = await fetch("https://html-to-pdf.eu/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.H2P_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ html: "<h1>Hello</h1><p>My first PDF.</p>" }),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
await writeFile("hello.pdf", Buffer.from(await response.arrayBuffer()));
console.log(response.headers.get("X-Credits-Remaining"));

Request body

Content type application/json. Provide either html or url.

FieldTypeDefaultDescription
htmlstringThe HTML to render. Either html or url is required.
urlstringA public http or https URL on port 80 or 443 to render instead of html.
headerHtmlstringHeader template, see below.
footerHtmlstringFooter template, see below.
formatstringA4A3, A4, A5, Letter or Legal.
landscapebooleanfalseLandscape orientation.
marginobject{ "top", "right", "bottom", "left" } as CSS lengths in mm, cm, in or px, for example "15mm".
printBackgroundbooleantruePrint background colours and images.
scalenumber1Page scale from 0.1 to 2.
waitForstringloadload or networkidle: when the page counts as ready.
delayMsinteger0Extra wait after that, 0 to 5000 ms.
fileNamestringDownload name, up to 200 characters, sent in Content-Disposition. .pdf is added when missing.

Headers and footers

Header and footer templates are only visible when the margin leaves room for them. With the default margin they may not show at all, so set margin.top and margin.bottom to at least 15mm when you use them. The templates can contain the classes pageNumber, totalPages, date and title, and need an explicit font size.

{
  "html": "<h1>Invoice 2026-001</h1>",
  "margin": { "top": "20mm", "bottom": "20mm", "left": "15mm", "right": "15mm" },
  "headerHtml": "<div style='font-size:9px;width:100%;text-align:center'>ACME B.V.</div>",
  "footerHtml": "<div style='font-size:9px;width:100%;text-align:center'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>"
}

Response

On success: 200 with Content-Type: application/pdf and the PDF as the body, plus these headers:

One successful PDF costs one credit. Failed renders cost nothing.

Errors

Errors are application/problem+json with a machine-readable code field.

StatuscodeMeaning
400 / 415invalid_requestMalformed JSON, or not application/json.
413invalid_requestThe raw request body is over 11 MB (twice the input limit plus 1 MB, room for JSON escaping).
401Missing or unknown API key.
402no_creditsYour credit balance is empty.
403The account is blocked.
422invalid_requestOne or more fields are invalid; errors lists them.
422url_not_allowedThe URL is not public http/https on port 80 or 443.
422too_many_pagesThe result would exceed 100 pages.
413output_too_largeThe generated PDF would exceed 25 MB.
429rate_limitedOver 60 requests per minute for this key. Retry-After says when the window resets.
429busyOver 3 concurrent requests for the account, or the rendering service is at capacity. Comes with Retry-After: 1; retry after a second. Nothing is charged.
502target_unreachableThe url could not be fetched.
503unavailableThe renderer is temporarily unavailable. Nothing is charged.
504timeoutRendering took longer than 30 seconds.
500render_failedThe render failed. Nothing is charged.

Limits

URLs

With url the page must be reachable on the public internet: http or https, port 80 or 443. Private networks, localhost and cloud-metadata addresses are refused. The same rule applies to everything the page loads while rendering. See how no storage works.