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.
| Field | Type | Default | Description |
|---|---|---|---|
html | string | The HTML to render. Either html or url is required. | |
url | string | A public http or https URL on port 80 or 443 to render instead of html. | |
headerHtml | string | Header template, see below. | |
footerHtml | string | Footer template, see below. | |
format | string | A4 | A3, A4, A5, Letter or Legal. |
landscape | boolean | false | Landscape orientation. |
margin | object | { "top", "right", "bottom", "left" } as CSS lengths in mm, cm, in or px, for example "15mm". | |
printBackground | boolean | true | Print background colours and images. |
scale | number | 1 | Page scale from 0.1 to 2. |
waitFor | string | load | load or networkidle: when the page counts as ready. |
delayMs | integer | 0 | Extra wait after that, 0 to 5000 ms. |
fileName | string | Download 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:
X-Credits-Remaining: your balance after this render.X-Pages: the number of pages in the PDF.
One successful PDF costs one credit. Failed renders cost nothing.
Errors
Errors are application/problem+json with a machine-readable code field.
| Status | code | Meaning |
|---|---|---|
| 400 / 415 | invalid_request | Malformed JSON, or not application/json. |
| 413 | invalid_request | The raw request body is over 11 MB (twice the input limit plus 1 MB, room for JSON escaping). |
| 401 | Missing or unknown API key. | |
| 402 | no_credits | Your credit balance is empty. |
| 403 | The account is blocked. | |
| 422 | invalid_request | One or more fields are invalid; errors lists them. |
| 422 | url_not_allowed | The URL is not public http/https on port 80 or 443. |
| 422 | too_many_pages | The result would exceed 100 pages. |
| 413 | output_too_large | The generated PDF would exceed 25 MB. |
| 429 | rate_limited | Over 60 requests per minute for this key. Retry-After says when the window resets. |
| 429 | busy | Over 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. |
| 502 | target_unreachable | The url could not be fetched. |
| 503 | unavailable | The renderer is temporarily unavailable. Nothing is charged. |
| 504 | timeout | Rendering took longer than 30 seconds. |
| 500 | render_failed | The render failed. Nothing is charged. |
Limits
- Input: 5 MB for
html,headerHtmlandfooterHtmltogether (422invalid_request). The raw JSON body is capped at 11 MB, twice the input limit plus 1 MB so escaped characters still fit (413). - Rendering time: 30 seconds.
- Pages: 100 per PDF.
- Output: 25 MB per PDF.
- Concurrency: 3 requests at the same time per account.
- Rate: 60 requests per minute per API key.
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.